@superpanel-sdk/runtime 0.5.0 → 0.7.0

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 (189) hide show
  1. package/README.md +3 -3
  2. package/dist/builder/codegen/emit.d.ts +5 -10
  3. package/dist/builder/codegen/emit.js +100 -106
  4. package/dist/builder/codegen/generate.d.ts +3 -5
  5. package/dist/builder/codegen/generate.js +13 -32
  6. package/dist/builder/company-imports.d.ts +5 -0
  7. package/dist/builder/company-imports.js +9 -1
  8. package/dist/builder/compile/compile-sequence.d.ts +13 -11
  9. package/dist/builder/compile/compile-sequence.js +135 -83
  10. package/dist/builder/compile/function-code.d.ts +15 -9
  11. package/dist/builder/compile/function-code.js +59 -29
  12. package/dist/builder/compile/registry.d.ts +4 -4
  13. package/dist/builder/compile/registry.js +14 -12
  14. package/dist/builder/errors.d.ts +9 -0
  15. package/dist/builder/errors.js +17 -0
  16. package/dist/builder/function-params.d.ts +10 -0
  17. package/dist/builder/function-params.js +203 -0
  18. package/dist/builder/load.d.ts +17 -17
  19. package/dist/builder/load.js +27 -25
  20. package/dist/builder/paths.d.ts +6 -1
  21. package/dist/builder/paths.js +16 -2
  22. package/dist/builder/schedules.d.ts +9 -7
  23. package/dist/builder/schedules.js +20 -21
  24. package/dist/builder/typecheck.d.ts +1 -1
  25. package/dist/builder/typecheck.js +3 -6
  26. package/dist/builder/types.d.ts +41 -5
  27. package/dist/builder/types.js +19 -0
  28. package/dist/cli/calls/index.d.ts +177 -0
  29. package/dist/cli/calls/index.js +96 -0
  30. package/dist/cli/debug/index.d.ts +31 -0
  31. package/dist/cli/debug/index.js +119 -0
  32. package/dist/cli/exec/index.d.ts +39 -0
  33. package/dist/cli/exec/index.js +138 -0
  34. package/dist/cli/exec/session.d.ts +48 -0
  35. package/dist/cli/exec/session.js +82 -0
  36. package/dist/cli/exec/trace.d.ts +18 -0
  37. package/dist/cli/exec/trace.js +52 -0
  38. package/dist/cli/index.js +84 -1
  39. package/dist/cli/logs/index.d.ts +47 -0
  40. package/dist/cli/logs/index.js +113 -0
  41. package/dist/cli/mobile/index.d.ts +12 -0
  42. package/dist/cli/mobile/index.js +405 -0
  43. package/dist/cli/serve/index.d.ts +40 -4
  44. package/dist/cli/serve/index.js +167 -28
  45. package/dist/cli/turns/index.d.ts +105 -0
  46. package/dist/cli/turns/index.js +128 -0
  47. package/dist/cli/turns/run.d.ts +13 -0
  48. package/dist/cli/turns/run.js +57 -0
  49. package/dist/cli/turns/shadow.d.ts +112 -0
  50. package/dist/cli/turns/shadow.js +54 -0
  51. package/dist/cli/type-gen/index.js +0 -1
  52. package/dist/compute-host/compute-host.d.ts +5 -6
  53. package/dist/compute-host/compute-host.js +4 -13
  54. package/dist/compute-host/source-compute-host.d.ts +5 -7
  55. package/dist/compute-host/source-compute-host.js +12 -33
  56. package/dist/core/case-orchestrator/contact-hours.d.ts +26 -6
  57. package/dist/core/case-orchestrator/contact-hours.js +27 -8
  58. package/dist/core/case-orchestrator/index.d.ts +1 -4
  59. package/dist/core/case-orchestrator/index.js +1 -4
  60. package/dist/core/case-orchestrator/render.d.ts +1 -1
  61. package/dist/core/case-orchestrator/render.js +15 -10
  62. package/dist/core/case-orchestrator/types.d.ts +83 -128
  63. package/dist/core/case-orchestrator/types.js +0 -4
  64. package/dist/core/context/email-message.d.ts +40 -1
  65. package/dist/core/context/email-message.js +27 -0
  66. package/dist/core/context/email.d.ts +13 -1
  67. package/dist/core/context/email.js +2 -3
  68. package/dist/core/context/exchanges.d.ts +25 -0
  69. package/dist/core/context/exchanges.js +25 -7
  70. package/dist/core/context/instances.d.ts +31 -2
  71. package/dist/core/context/instances.js +18 -4
  72. package/dist/core/context/platform-registry.d.ts +35 -0
  73. package/dist/core/context/sms.d.ts +30 -0
  74. package/dist/core/context/sms.js +20 -0
  75. package/dist/core/context/types.d.ts +29 -1
  76. package/dist/core/conversation/index.d.ts +58 -4
  77. package/dist/core/conversation/index.js +123 -4
  78. package/dist/core/functions/errors.d.ts +74 -0
  79. package/dist/core/functions/errors.js +26 -0
  80. package/dist/core/functions/events.d.ts +23 -78
  81. package/dist/core/functions/events.js +8 -17
  82. package/dist/core/functions/schedules.d.ts +9 -10
  83. package/dist/core/functions/schedules.js +2 -2
  84. package/dist/core/functions/types.d.ts +17 -72
  85. package/dist/core/functions/types.js +1 -9
  86. package/dist/core/page/define.d.ts +230 -160
  87. package/dist/core/page/define.js +145 -104
  88. package/dist/core/page/envelope.d.ts +55 -0
  89. package/dist/core/page/envelope.js +4 -0
  90. package/dist/core/page/fns.d.ts +448 -0
  91. package/dist/core/page/fns.js +539 -0
  92. package/dist/core/page/memory-reader.d.ts +27 -0
  93. package/dist/core/page/memory-reader.js +299 -0
  94. package/dist/core/page/page-host.d.ts +20 -0
  95. package/dist/core/page/page-host.js +57 -0
  96. package/dist/core/page/records-query.d.ts +234 -0
  97. package/dist/core/page/records-query.js +780 -0
  98. package/dist/core/page/render.d.ts +67 -25
  99. package/dist/core/page/render.js +492 -272
  100. package/dist/core/page/sandbox.d.ts +85 -0
  101. package/dist/core/page/sandbox.js +357 -0
  102. package/dist/core/page/time-format.d.ts +27 -0
  103. package/dist/core/page/time-format.js +84 -0
  104. package/dist/core/page/typed.d.ts +165 -159
  105. package/dist/core/page/workspace.d.ts +25 -1
  106. package/dist/core/page/workspace.js +48 -13
  107. package/dist/core/party/schema.d.ts +74 -3
  108. package/dist/core/party/schema.js +39 -6
  109. package/dist/core/party/types.d.ts +8 -13
  110. package/dist/core/sequence/define.d.ts +1 -2
  111. package/dist/core/slot/company-metadata.d.ts +11 -22
  112. package/dist/core/slot/company-metadata.js +12 -39
  113. package/dist/core/slot/compile-schema.js +6 -6
  114. package/dist/core/slot/field.d.ts +1 -2
  115. package/dist/core/slot/field.js +0 -2
  116. package/dist/core/slot/index.d.ts +3 -3
  117. package/dist/core/slot/index.js +3 -3
  118. package/dist/core/slot/naming.d.ts +1 -1
  119. package/dist/core/slot/naming.js +1 -1
  120. package/dist/core/slot/platform-fields.d.ts +4 -4
  121. package/dist/core/slot/platform-fields.js +5 -5
  122. package/dist/core/slot/schema-compat.d.ts +8 -7
  123. package/dist/core/slot/schema-compat.js +22 -31
  124. package/dist/core/slot/schema.d.ts +3 -59
  125. package/dist/core/slot/schema.js +0 -15
  126. package/dist/core/slot/types.d.ts +4 -10
  127. package/dist/core/slot/value.d.ts +2 -0
  128. package/dist/core/slot/value.js +6 -2
  129. package/dist/dev-overlay/protocol.d.ts +38 -5
  130. package/dist/fn-context/build-context.js +10 -4
  131. package/dist/fn-context/fake/index.d.ts +2 -0
  132. package/dist/fn-context/fake/index.js +5 -3
  133. package/dist/fn-context/fn-params.d.ts +2 -5
  134. package/dist/fn-context/fn-params.js +6 -9
  135. package/dist/fn-context/index.d.ts +1 -1
  136. package/dist/fn-context/make-ctx.js +5 -4
  137. package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +11 -3
  138. package/dist/fn-context/rpc/case-orchestrator-tools.js +0 -1
  139. package/dist/fn-context/rpc/errors.d.ts +13 -0
  140. package/dist/fn-context/rpc/errors.js +22 -0
  141. package/dist/fn-context/rpc/index.d.ts +1 -1
  142. package/dist/fn-context/rpc/index.js +1 -1
  143. package/dist/fn-context/rpc/integration/index.d.ts +11 -1
  144. package/dist/fn-context/rpc/log.d.ts +11 -1
  145. package/dist/fn-context/rpc/platform.d.ts +11 -1
  146. package/dist/fn-context/rpc/records.d.ts +13 -6
  147. package/dist/fn-context/rpc/records.js +2 -4
  148. package/dist/fn-context/rpc/trpc-client.d.ts +42 -0
  149. package/dist/fn-context/rpc/trpc-client.js +151 -22
  150. package/dist/fn-context/rpc/trpc-init.d.ts +19 -1
  151. package/dist/fn-context/rpc/trpc-init.js +10 -4
  152. package/dist/fn-context/rpc/trpc-router-factory.d.ts +66 -8
  153. package/dist/fn-context/run-scope.js +1 -1
  154. package/dist/fn-context/types.d.ts +20 -6
  155. package/dist/index.d.ts +11 -12
  156. package/dist/index.js +5 -8
  157. package/dist/logger.d.ts +4 -0
  158. package/dist/logger.js +8 -0
  159. package/dist/platform.d.ts +32 -33
  160. package/dist/platform.js +23 -22
  161. package/dist/testing-compile-child.d.ts +2 -0
  162. package/dist/testing-compile-child.js +20 -0
  163. package/dist/testing.d.ts +368 -0
  164. package/dist/testing.js +439 -0
  165. package/dist/voice-service/actions.d.ts +12 -35
  166. package/dist/voice-service/index.d.ts +2 -0
  167. package/dist/voice-service/index.js +2 -0
  168. package/dist/voice-service/protocol.d.ts +604 -0
  169. package/dist/voice-service/protocol.js +67 -0
  170. package/dist/voice-service/types.d.ts +16 -6
  171. package/dist/worker.d.ts +2 -1
  172. package/dist/worker.js +2 -1
  173. package/package.json +20 -16
  174. package/dist/core/case-orchestrator/follow-ups.d.ts +0 -31
  175. package/dist/core/case-orchestrator/follow-ups.js +0 -103
  176. package/dist/core/case-orchestrator/tools.d.ts +0 -29
  177. package/dist/core/case-orchestrator/tools.js +0 -31
  178. package/dist/core/case-orchestrator/validate.d.ts +0 -11
  179. package/dist/core/case-orchestrator/validate.js +0 -63
  180. package/dist/core/functions/define/bind.d.ts +0 -25
  181. package/dist/core/functions/define/bind.js +0 -14
  182. package/dist/core/functions/define/case.d.ts +0 -9
  183. package/dist/core/functions/define/case.js +0 -13
  184. package/dist/core/functions/define/events.d.ts +0 -20
  185. package/dist/core/functions/define/events.js +0 -21
  186. package/dist/core/functions/define/no-case.d.ts +0 -4
  187. package/dist/core/functions/define/no-case.js +0 -9
  188. package/dist/core/functions/func-ref.d.ts +0 -25
  189. package/dist/core/functions/func-ref.js +0 -20
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Test helpers for a company repository's unit tests (`sequences/<id>/tests/*.test.ts`): a fake `ctx`,
3
+ * a `db` that records its calls, a `fetch` that answers from routes, and the page rendered from rows the
4
+ * test gives. Everything runs in process: no server, no network, no model, nothing written.
5
+ *
6
+ * Only test files import this entry. The compile loads a repository from its entries, which never reach
7
+ * a test file, and refuses `@superpanel-sdk/runtime/testing` in any file it does load
8
+ * (`builder/company-imports.ts`).
9
+ */
10
+ import { fork } from 'node:child_process';
11
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { after, afterEach, beforeEach } from 'node:test';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { CONTEXT_PAGE_FUNCTION, functionKey, functionScriptFile } from './builder/types.js';
16
+ import { apiUrl } from './cli/auth/http.js';
17
+ import { openExecSession } from './cli/exec/session.js';
18
+ import { openLogReader } from './cli/logs/index.js';
19
+ import { openOverlay } from './cli/serve/index.js';
20
+ import { openTextCall, testCustomer, testCustomerPhone } from './cli/calls/index.js';
21
+ import { channelOf, readTurnExport, writeTurnExport } from './cli/turns/index.js';
22
+ import { checkPageStart, emptyRuns } from './core/page/define.js';
23
+ import { createMemoryReader } from './core/page/memory-reader.js';
24
+ import { atCall, dayCall, partsCall, timeCall, timeOfCaller } from './core/page/page-host.js';
25
+ import { renderDocument, RowNames } from './core/page/render.js';
26
+ import { runInSandbox } from './core/page/sandbox.js';
27
+ import { workspaceText } from './core/page/workspace.js';
28
+ import { parseOfficeHours, resolveOfficeHours } from './core/office-hours/index.js';
29
+ import { makeCaseOrchestratorCtx as makePlatformCaseOrchestratorCtx, makeCtx as makePlatformCtx } from './fn-context/make-ctx.js';
30
+ import { shadowTurn } from './cli/turns/shadow.js';
31
+ import { casesIn, writeDebugPackage } from './cli/debug/index.js';
32
+ export { fakeTools, unfaked } from './fn-context/fake/index.js';
33
+ /**
34
+ * A fake `ctx`, typed as the caller asks: `makeCtx<Ctx>()` for a function, with `Ctx` from the
35
+ * sequence's `__gen`. Data slice defaults: `today` 1970-01-01, `timezone` UTC. Any `api` call the test
36
+ * did not fake rejects, naming the call.
37
+ */
38
+ export function makeCtx(overrides = {}) {
39
+ return makePlatformCtx(overrides);
40
+ }
41
+ /** A fake Orchestrator `ctx`, typed as the caller asks; `tools` fakes `ctx.api.tools`, `db` fakes `ctx.api.db`. */
42
+ export async function makeCaseOrchestratorCtx(overrides = {}) {
43
+ return (await makePlatformCaseOrchestratorCtx(overrides));
44
+ }
45
+ /**
46
+ * A `db` that records every call in `calls`, in order, and answers from `handlers`. A call with no
47
+ * handler rejects, naming it, so a test sees every read and write the function makes.
48
+ */
49
+ export function recordingDb(handlers = {}) {
50
+ const calls = [];
51
+ const db = new Proxy({}, {
52
+ get: (_, table) => new Proxy({}, {
53
+ get: (__, method) => async (args) => {
54
+ calls.push({ table: String(table), method: String(method), args });
55
+ const handler = handlers[String(table)]?.[String(method)];
56
+ if (!handler)
57
+ throw new Error(`recordingDb: db.${String(table)}.${String(method)} has no handler`);
58
+ return handler(args);
59
+ },
60
+ }),
61
+ });
62
+ return { db, calls };
63
+ }
64
+ /**
65
+ * A row as `ctx.api.db` hands it out, for a `recordingDb` handler to return: `id` as given, every other
66
+ * field a slot (`{ value, state, reason, updatedAt, ref }`), `filled` when it has a value, `empty` when
67
+ * null. A test that returns plain values instead (`{ crm_id: null }`) checks code against a shape the
68
+ * platform never gives: `if (row.crm_id)` is then false in the test and always true for real.
69
+ */
70
+ export function row(table, values) {
71
+ const out = { id: values.id };
72
+ for (const [field, value] of Object.entries(values)) {
73
+ if (field === 'id')
74
+ continue;
75
+ if (field === 'createdAt' || field === 'updatedAt') {
76
+ out[field] = value;
77
+ continue;
78
+ }
79
+ const empty = value === null || value === undefined;
80
+ out[field] = { value: empty ? null : value, state: empty ? 'empty' : 'filled', reason: null, updatedAt: null, ref: { table, id: values.id, field } };
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * Replaces the global `fetch` until `restore()`: each request goes to the first matching route; one
86
+ * that matches none rejects, so a test never reaches a real system (a CRM, a payment provider).
87
+ */
88
+ export function mockFetch(routes) {
89
+ const original = globalThis.fetch;
90
+ const calls = [];
91
+ globalThis.fetch = async (input, init) => {
92
+ const request = new Request(input, init);
93
+ const body = request.body ? await request.clone().text() : null;
94
+ calls.push({ method: request.method, url: request.url, body });
95
+ const route = routes.find((r) => (!r.method || r.method.toUpperCase() === request.method) &&
96
+ (typeof r.url === 'string' ? r.url === request.url : r.url.test(request.url)));
97
+ if (!route)
98
+ throw new Error(`mockFetch: no route for ${request.method} ${request.url}`);
99
+ const answer = await route.respond(request);
100
+ if (answer instanceof Response)
101
+ return answer;
102
+ return new Response(JSON.stringify(answer), { status: 200, headers: { 'content-type': 'application/json' } });
103
+ };
104
+ return {
105
+ calls,
106
+ restore: () => {
107
+ globalThis.fetch = original;
108
+ },
109
+ };
110
+ }
111
+ /** A page script that failed: what the sandbox said, and where in the compiled page it threw. */
112
+ export class PageTestError extends Error {
113
+ scriptStack;
114
+ constructor(message, scriptStack) {
115
+ super(message);
116
+ this.scriptStack = scriptStack;
117
+ this.name = 'PageTestError';
118
+ if (scriptStack)
119
+ this.stack = `${this.name}: ${message}\n${scriptStack}`;
120
+ }
121
+ }
122
+ /**
123
+ * Renders the sequence's page (`context.page.ts`) over `data`, as the platform renders it: the same
124
+ * compile, sandbox, `db` semantics and page building, with the rows read from `data` instead of the
125
+ * database. A read the platform refuses is refused here too.
126
+ */
127
+ export async function renderPage(input) {
128
+ return runTestPage(input, 'render', (value, reader, schema, build, out) => {
129
+ const page = renderDocument({
130
+ document: value,
131
+ schema,
132
+ reads: { row: (id) => reader.rowInfo(id), parentsOf: (table) => reader.parentsOf(table) },
133
+ names: new RowNames(),
134
+ ...(input.channel ? { channel: input.channel } : {}),
135
+ ...(build.contextPage.functions ? { functions: build.contextPage.functions } : {}),
136
+ ...(build.contextPage.actions ? { actions: build.contextPage.actions } : {}),
137
+ });
138
+ return { page, text: page.auto ? '' : workspaceText(page), reads: reader.calls, console: out };
139
+ });
140
+ }
141
+ /**
142
+ * A turn's export (`turns export <turnId>`, `exportTurn`) as `renderPage`'s input: the moment the turn
143
+ * began — its rows, its `ctx`, and each `ctx.history` answer — to render with this repository's code now.
144
+ */
145
+ export function fromTurn(dir, options = {}) {
146
+ const { turn, snapshot } = readTurnExport(path.resolve(options.repoRoot ?? process.cwd(), dir));
147
+ if (!snapshot?.context)
148
+ throw new Error(`${dir}: the turn's snapshot is not there (it is kept 30 days): only turn.json`);
149
+ const c = snapshot.context;
150
+ const caller = c.caller?.id;
151
+ return {
152
+ sequence: turn.sequence,
153
+ ...(options.repoRoot ? { repoRoot: options.repoRoot } : {}),
154
+ data: snapshot.data,
155
+ subject: String(c.subject ?? ''),
156
+ caller: typeof caller === 'string' ? caller : null,
157
+ channel: c.channel ?? channelOf(turn),
158
+ ...(c.now ? { now: c.now } : {}),
159
+ scheduledAction: c.scheduledAction ?? null,
160
+ officeHours: c.officeHours ?? null,
161
+ nextSteps: c.nextSteps ?? [],
162
+ ...(c.sms ? { sms: c.sms } : {}),
163
+ ...(c.email ? { email: c.email } : {}),
164
+ ...(c.call ? { call: c.call } : {}),
165
+ history: c.history ?? [],
166
+ session: c.session ?? null,
167
+ ...(c.runs ? { runs: c.runs } : {}),
168
+ conversation: c.conversation ?? null,
169
+ ...(c.instanceId ? { instanceId: c.instanceId } : {}),
170
+ };
171
+ }
172
+ /** What the page decides when the conversation starts (its `persona` and `faq`), over `data`. */
173
+ export async function startPage(input) {
174
+ return runTestPage(input, 'start', (value) => checkPageStart(value));
175
+ }
176
+ const builds = new Map();
177
+ /**
178
+ * The sequence compiled once per test process (the compile type-checks the repository), in a child
179
+ * process (`testing-compile-child.ts`): the company's code it loads stays out of the test's process and
180
+ * its coverage, which then counts only what the tests ran.
181
+ */
182
+ function compiled(repoRoot, sequence) {
183
+ const key = `${repoRoot}\u0000${sequence}`;
184
+ let hit = builds.get(key);
185
+ if (!hit) {
186
+ hit = compileInChild(repoRoot, sequence);
187
+ builds.set(key, hit);
188
+ hit.catch(() => builds.delete(key));
189
+ }
190
+ return hit;
191
+ }
192
+ function compileInChild(repoRoot, sequence) {
193
+ const child = fileURLToPath(new URL('./testing-compile-child.js', import.meta.url));
194
+ const { NODE_V8_COVERAGE: _coverage, ...env } = process.env;
195
+ return new Promise((resolve, reject) => {
196
+ // `.js` next to this file when built; the runtime's own tests run the `.ts` source through tsx.
197
+ const script = existsSync(child) ? child : child.replace(/\.js$/, '.ts');
198
+ const proc = fork(script, [repoRoot, sequence], { env, execArgv: script.endsWith('.ts') ? ['--import', 'tsx'] : [], stdio: ['ignore', 'ignore', 'pipe', 'ipc'] });
199
+ let stderr = '';
200
+ let answered = false;
201
+ proc.stderr?.on('data', (chunk) => (stderr += String(chunk)));
202
+ proc.on('message', (message) => {
203
+ answered = true;
204
+ if (message.ok)
205
+ resolve({ build: message.build, schema: message.schema });
206
+ else
207
+ reject(new Error(message.error));
208
+ proc.kill();
209
+ });
210
+ proc.on('exit', (code) => {
211
+ if (!answered)
212
+ reject(new Error(`compiling sequences/${sequence} failed (exit ${code})${stderr ? `: ${stderr.trim()}` : ''}`));
213
+ });
214
+ });
215
+ }
216
+ async function runTestPage(input, mode, done) {
217
+ const repoRoot = path.resolve(input.repoRoot ?? process.cwd());
218
+ const { build, schema } = await compiled(repoRoot, input.sequence);
219
+ const now = new Date(input.now ?? '2026-01-01T15:00:00.000Z');
220
+ const reader = createMemoryReader({ schema, rows: input.data, now: now.getTime() });
221
+ const caller = input.caller ? reader.row('contact', input.caller) : null;
222
+ if (input.caller && !caller)
223
+ throw new Error(`renderPage: caller ${input.caller} is not in data.contact`);
224
+ const arg = {
225
+ mode,
226
+ channel: input.channel ?? null,
227
+ subject: input.subject,
228
+ instanceId: input.instanceId ?? 'test-instance',
229
+ caller: caller,
230
+ session: input.session ?? null,
231
+ conversation: input.conversation ?? null,
232
+ runs: input.runs ?? emptyRuns(),
233
+ scheduledAction: input.scheduledAction ?? null,
234
+ officeHours: input.officeHours ?? null,
235
+ nextSteps: input.nextSteps ?? [],
236
+ now: now.toISOString(),
237
+ time: timeOfCaller(caller, now),
238
+ env: input.env ?? {},
239
+ ...(input.sms ? { sms: input.sms } : {}),
240
+ ...(input.email ? { email: input.email } : {}),
241
+ ...(input.channel === 'call' ? { call: { previous: null, farEnd: null, phase: 'live', ...input.call } } : {}),
242
+ };
243
+ const out = [];
244
+ const history = input.history ?? {};
245
+ const answers = Array.isArray(history) ? history : null;
246
+ const counts = Array.isArray(history) ? {} : history;
247
+ const dispatch = async (calls) => {
248
+ const reads = [];
249
+ const results = calls.map((call, index) => {
250
+ if (call.fn === 'time')
251
+ return timeCall(call.args);
252
+ if (call.fn === 'day')
253
+ return dayCall(call.args);
254
+ if (call.fn === 'at')
255
+ return atCall(call.args);
256
+ if (call.fn === 'parts')
257
+ return partsCall(call.args);
258
+ if (call.fn === 'history') {
259
+ const [method, where] = call.args;
260
+ if (answers) {
261
+ const key = JSON.stringify(where ?? null);
262
+ const found = answers.find((a) => a.method === method && JSON.stringify(a.where ?? null) === key);
263
+ return found ? found.result : { ok: false, error: `history.${String(method)}(${key}): not among the answers given (the turn's page asked something else)` };
264
+ }
265
+ if (method === 'timeline')
266
+ return { ok: true, value: counts.timeline ?? [] };
267
+ if (method === 'sessionCount')
268
+ return { ok: true, value: counts.sessionCount ?? 0 };
269
+ if (method === 'exchangeCount')
270
+ return { ok: true, value: counts.exchangeCount ?? 0 };
271
+ return { ok: false, error: `history.${String(method)}: not a history count` };
272
+ }
273
+ if (call.fn !== 'records')
274
+ return { ok: false, error: `${call.fn}: not available here (a page only reads)` };
275
+ const [table, method, args] = call.args;
276
+ reads.push({ index, call: { table, method, args } });
277
+ return { ok: true, value: null };
278
+ });
279
+ const answered = await reader.execute(reads.map((r) => r.call));
280
+ reads.forEach((r, i) => (results[r.index] = answered[i]));
281
+ return results;
282
+ };
283
+ let sandbox;
284
+ try {
285
+ sandbox = await runInSandbox({
286
+ code: build.contextPage.code,
287
+ arg,
288
+ dispatch,
289
+ label: 'context.page.ts',
290
+ file: functionScriptFile(functionKey(input.sequence, CONTEXT_PAGE_FUNCTION)),
291
+ console: (level, text) => out.push({ level, text }),
292
+ });
293
+ }
294
+ catch (error) {
295
+ const stack = error.scriptStack;
296
+ throw new PageTestError(error instanceof Error ? error.message : String(error), typeof stack === 'string' ? stack : null);
297
+ }
298
+ return done(sandbox.value, reader, schema, build, out);
299
+ }
300
+ // ---------------------------------------------------------------------------------------------
301
+ // Configuration: office hours, the sequence as compiled
302
+ /**
303
+ * The office hours (`office-hours.ts`) at `at` (ISO 8601 or epoch ms), as the platform reads them: open
304
+ * or not, and today's windows. Office hours the platform would refuse fail with what is wrong.
305
+ */
306
+ export function officeHoursAt(officeHours, at) {
307
+ const parsed = parseOfficeHours(officeHours);
308
+ if (!parsed.ok)
309
+ throw new Error(`office hours: ${parsed.issues.join('; ')}`);
310
+ const now = typeof at === 'number' ? at : Date.parse(at);
311
+ if (Number.isNaN(now))
312
+ throw new Error(`officeHoursAt: ${String(at)} is not a time`);
313
+ return resolveOfficeHours(parsed.value, now);
314
+ }
315
+ /**
316
+ * The sequence compiled as a release compiles it (`index.ts`, its variables, functions, events, office
317
+ * hours, Orchestrator and page), for a test that checks its configuration. A configuration the release
318
+ * would refuse rejects with the release's message. Compiled once per test run.
319
+ */
320
+ export async function compileForTest(input) {
321
+ return (await compiled(path.resolve(input.repoRoot ?? process.cwd()), input.sequence)).build;
322
+ }
323
+ /**
324
+ * Connects a test file to an environment (`sandbox` when `env` is left out) as the CLI's login: this
325
+ * repository's code serves it (as `superpanel-sdk-runtime serve` does, its imports checked as a release
326
+ * checks them) until the file's tests are done, and `ctx` reads and writes the environment's real data.
327
+ * What a test writes is written.
328
+ */
329
+ /**
330
+ * `debug`: when a test's debug package is written (68 §9, `.superpanel/tests/`): `failed` (the default),
331
+ * `always`, or `off`; `SUPERPANEL_TEST_DEBUG` overrides it.
332
+ */
333
+ export async function connect(options = {}) {
334
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
335
+ const openedAt = new Date();
336
+ // Kept on disk, not in memory: a long test file makes many calls.
337
+ const dir = path.join(repoRoot, '.superpanel', 'integration', `${openedAt.toISOString().replace(/[:.]/g, '-')}-${process.pid}`);
338
+ mkdirSync(dir, { recursive: true });
339
+ const callsFile = path.join(dir, 'calls.jsonl');
340
+ writeFileSync(callsFile, '');
341
+ const calls = [];
342
+ // The test running now: when it started, the cases it named or was answered with, its `ctx.api` calls.
343
+ let current = { startedAt: new Date().toISOString(), cases: new Set(), calls: [] };
344
+ const record = (call) => {
345
+ appendFileSync(callsFile, `${JSON.stringify(call)}\n`);
346
+ calls.push(call);
347
+ if (calls.length > 200)
348
+ calls.splice(0, calls.length - 200);
349
+ current.calls.push(call);
350
+ casesIn(call.input, current.cases);
351
+ casesIn(call.output, current.cases);
352
+ };
353
+ const session = await openExecSession(repoRoot, { env: options.env, url: options.url, record });
354
+ const readLogs = await openLogReader(repoRoot, options.url);
355
+ const overlay = await openOverlay(repoRoot, { backendUrl: session.backendUrl, token: session.loginToken, environmentId: session.run.environment.id });
356
+ let closed = false;
357
+ const close = async () => {
358
+ if (closed)
359
+ return;
360
+ closed = true;
361
+ session.close();
362
+ await overlay.close();
363
+ };
364
+ // A test file's process ends once nothing is open: the serving connection closes after its tests.
365
+ try {
366
+ after(close);
367
+ }
368
+ catch {
369
+ // not in a test run: the caller closes it
370
+ }
371
+ const debug = process.env.SUPERPANEL_TEST_DEBUG ?? options.debug ?? 'failed';
372
+ const debugRoot = path.join(repoRoot, '.superpanel', 'tests', path.basename(dir));
373
+ try {
374
+ beforeEach(() => {
375
+ current = { startedAt: new Date().toISOString(), cases: new Set(), calls: [] };
376
+ });
377
+ afterEach(async (t) => {
378
+ const test = current;
379
+ if (debug === 'off' || (debug === 'failed' && t.passed !== false))
380
+ return;
381
+ // The code's log lines reach the store a few seconds after they are written.
382
+ if (!t.passed)
383
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
384
+ const written = await writeDebugPackage(debugRoot, { name: t.fullName, error: t.passed ? null : t.error, startedAt: test.startedAt, cases: [...test.cases], calls: test.calls }, {
385
+ caseDebug: (instanceId) => post('debug', { instanceId }),
386
+ readTurn: (turnId) => post('turns/read', { turnId }),
387
+ logs: async (instanceId, since) => (await readLogs({ env: session.run.environment.id, instance: instanceId, since, order: 'asc', limit: 500 })).lines,
388
+ });
389
+ console.log(`debug package: ${path.relative(repoRoot, written)}/summary.md`);
390
+ });
391
+ }
392
+ catch {
393
+ // not in a test run
394
+ }
395
+ const post = async (what, body) => {
396
+ // Waits as long as the backend may take: an Orchestrator run or a turn is slower than a request.
397
+ const send = async (force) => fetch(apiUrl(session.backendUrl, `cli-auth/exec/${what}`), {
398
+ method: 'POST',
399
+ headers: { Authorization: `Bearer ${await session.token(force)}`, 'Content-Type': 'application/json' },
400
+ body: JSON.stringify(body),
401
+ signal: AbortSignal.timeout(180_000),
402
+ });
403
+ let response = await send(false);
404
+ // Its token ran out after all (a process asleep past the renewal): renewed, sent once more.
405
+ if (response.status === 401)
406
+ response = await send(true);
407
+ const payload = (await response.json().catch(() => ({})));
408
+ casesIn(body, current.cases);
409
+ if (!response.ok)
410
+ throw new Error(`${what}: ${payload.error ?? `the backend answered ${response.status}`}`);
411
+ if (what !== 'debug')
412
+ casesIn(payload, current.cases);
413
+ return payload;
414
+ };
415
+ return {
416
+ environment: session.run.environment,
417
+ ctx: session.ctx,
418
+ calls,
419
+ callsFile,
420
+ logs: async (query = {}) => (await readLogs({ ...query, env: session.run.environment.id, since: query.since ?? openedAt.toISOString(), order: 'asc' })).lines,
421
+ renderPage: (input) => post('page', input),
422
+ runOrchestrator: (input) => post('orchestrator', input),
423
+ turn: (input) => shadowTurn(post, repoRoot, 'turn', input),
424
+ act: (input) => shadowTurn(post, repoRoot, 'act', input),
425
+ turns: async (query) => (await post('turns', query)).turns,
426
+ readTurn: (turnId) => post('turns/read', { turnId }),
427
+ sim: {
428
+ line: (name) => post('sim/line', name ? { name } : {}),
429
+ mailbox: (name) => post('sim/mailbox', name ? { name } : {}),
430
+ phone: testCustomerPhone,
431
+ customer: (input) => testCustomer(post, input),
432
+ },
433
+ runDue: (input) => post('run-due', input),
434
+ call: { inbound: (input) => openTextCall(post, { from: input.from, to: input.to ?? 'Main line' }) },
435
+ exportTurn: async (turnId, dir) => writeTurnExport(path.resolve(repoRoot, dir ?? path.join('.superpanel', 'turns', turnId)), await post('turns/read', { turnId })),
436
+ close,
437
+ };
438
+ }
439
+ //# sourceMappingURL=testing.js.map
@@ -1,18 +1,16 @@
1
- import type { CallEndEvent, CallRecordedEvent, CallStatusChangeEvent, ChatItemWithCallSid, FieldCollectResponse, IncomingOutgoingCall, TransferEvent } from './types.js';
1
+ import type { CallEndEvent, CallRecordedEvent, CallStatusChangeEvent, IncomingOutgoingCall } from './types.js';
2
+ import type * as VoiceProtocol from './protocol.js';
2
3
  /** Superpanel SDK-owned action dispatch types. Do not add to types.ts — that file is overwritten from Mars. */
3
- export type VoiceServiceActionName = 'fetchTwilioConfig' | 'onInbound' | 'onCallStart' | 'onCallEnd' | 'onCallStatusChanged' | 'onChatItem' | 'onOutbound' | 'onTransferEvents' | 'onFieldCollectCompleted' | 'onFarEnd' | 'onCallRecorded' | 'onSms';
4
+ export type VoiceServiceActionName = 'fetchTwilioConfig' | 'onInbound' | 'onCallEnd' | 'onCallStatusChanged' | 'onOutbound' | 'onCallRecorded' | 'onCallEvents' | 'onSms';
4
5
  export type VoiceServiceActionMap = {
5
6
  fetchTwilioConfig: IncomingOutgoingCall;
6
7
  onInbound: IncomingOutgoingCall;
7
- onCallStart: IncomingOutgoingCall;
8
8
  onCallEnd: CallEndEvent;
9
9
  onCallStatusChanged: CallStatusChangeEvent;
10
- onChatItem: ChatItemWithCallSid & WithEventId;
11
10
  onOutbound: IncomingOutgoingCall;
12
- onTransferEvents: TransferEvent & WithEventId;
13
- onFieldCollectCompleted: FieldCollectResponse & WithEventId;
14
- onFarEnd: FarEndEvent & WithEventId;
15
11
  onCallRecorded: CallRecordedEvent;
12
+ /** Upstream protocol events the call's socket never got acked: sent once the socket is gone. */
13
+ onCallEvents: VoiceProtocol.CallEventsBatch;
16
14
  onSms: {
17
15
  MessageSid: string;
18
16
  AccountSid: string;
@@ -31,31 +29,11 @@ export type WithEventId = {
31
29
  eventId?: string;
32
30
  };
33
31
  /**
34
- * A call event the voice service sends on the call's socket (`{ action: 'event', params }`), answered with
35
- * `eventAck` once handled and saved. Once the call's socket is gone it goes by HTTP as its own action.
32
+ * The voice service's word on who answered an outbound call (65 §3.1): `voicemail`, a voicemail greeting;
33
+ * `human`, a person after all — a voicemail verdict taken back before the message started. Anything else
34
+ * (a person, a call-screening assistant, a phone menu) is the page's model's to tell, from the words.
36
35
  */
37
- export type VoiceCallEvent = {
38
- eventId: string;
39
- name: 'chatItem';
40
- payload: ChatItemWithCallSid;
41
- } | {
42
- eventId: string;
43
- name: 'fieldCollectCompleted';
44
- payload: FieldCollectResponse;
45
- } | {
46
- eventId: string;
47
- name: 'transferEvent';
48
- payload: TransferEvent;
49
- } | {
50
- eventId: string;
51
- name: 'farEnd';
52
- payload: FarEndEvent;
53
- };
54
- /**
55
- * Who answered an outbound call (25 §13): a person, a voicemail greeting, a call-screening assistant
56
- * ("say your name and why you are calling"), or a phone menu. The voice service tells; the page decides.
57
- */
58
- export type FarEndKind = 'human' | 'voicemail' | 'screening' | 'ivr';
36
+ export type FarEndKind = 'human' | 'voicemail';
59
37
  /** The voice service's verdict on the far end, with what it said. */
60
38
  export type FarEndEvent = {
61
39
  callSid: string;
@@ -63,14 +41,13 @@ export type FarEndEvent = {
63
41
  said: string;
64
42
  };
65
43
  /**
66
- * The backend's answer to a far end (25 §13): `handled` — the page took it (it leaves a message, speaks to
67
- * the screener, presses keys, ends), so the voice service does not do its own thing (and, screened,
68
- * waits longer in silence); `default` — the voice service does what it did before (hang up on a machine).
44
+ * The backend's answer to a voicemail (25 §13): `handled` — the page took it (it leaves a message or ends),
45
+ * so the voice service does not do its own thing; `default` — the voice service hangs up.
69
46
  */
70
47
  export type FarEndDecisionRequest = {
71
48
  callSid: string;
72
49
  type: 'far-end-decision';
73
- kind: FarEndKind;
50
+ kind: 'voicemail';
74
51
  decision: 'default' | 'handled';
75
52
  };
76
53
  /** Leave this message on the voicemail, after its beep, word for word, then hang up (25 §13). */
@@ -1,3 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './actions.js';
3
+ /** The call protocol with the voice service (copied from jupiter-voice-service `src/protocol/voice-protocol.ts`). */
4
+ export * as VoiceProtocol from './protocol.js';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './actions.js';
3
+ /** The call protocol with the voice service (copied from jupiter-voice-service `src/protocol/voice-protocol.ts`). */
4
+ export * as VoiceProtocol from './protocol.js';
3
5
  //# sourceMappingURL=index.js.map