@obov/mext 0.5.0 → 0.6.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 (157) hide show
  1. package/dist/approval.d.ts +33 -0
  2. package/dist/approval.js +36 -0
  3. package/dist/approval.js.map +1 -0
  4. package/dist/audit.d.ts +123 -0
  5. package/dist/audit.js +221 -0
  6. package/dist/audit.js.map +1 -0
  7. package/dist/build-cli.d.ts +1 -0
  8. package/dist/build-cli.js.map +1 -1
  9. package/dist/build.d.ts +2 -0
  10. package/dist/build.js +30 -3
  11. package/dist/build.js.map +1 -1
  12. package/dist/caller.d.ts +53 -0
  13. package/dist/caller.js +51 -0
  14. package/dist/caller.js.map +1 -0
  15. package/dist/cli/build.js +4 -2
  16. package/dist/cli/build.js.map +1 -1
  17. package/dist/cli/dev-audit.d.ts +25 -0
  18. package/dist/cli/dev-audit.js +84 -0
  19. package/dist/cli/dev-audit.js.map +1 -0
  20. package/dist/cli/dev-lock.d.ts +3 -1
  21. package/dist/cli/dev-lock.js +12 -5
  22. package/dist/cli/dev-lock.js.map +1 -1
  23. package/dist/cli/dev-server.d.ts +9 -0
  24. package/dist/cli/dev-server.js +24 -1
  25. package/dist/cli/dev-server.js.map +1 -1
  26. package/dist/cli/dev-tap.d.ts +20 -0
  27. package/dist/cli/dev-tap.js +70 -0
  28. package/dist/cli/dev-tap.js.map +1 -0
  29. package/dist/cli/dev.js +16 -4
  30. package/dist/cli/dev.js.map +1 -1
  31. package/dist/cli/help.js +2 -0
  32. package/dist/cli/help.js.map +1 -1
  33. package/dist/cli/inspect.d.ts +7 -0
  34. package/dist/cli/inspect.js +699 -0
  35. package/dist/cli/inspect.js.map +1 -0
  36. package/dist/cli/main.js +8 -0
  37. package/dist/cli/main.js.map +1 -1
  38. package/dist/cli/output.d.ts +1 -1
  39. package/dist/cli/output.js +3 -2
  40. package/dist/cli/output.js.map +1 -1
  41. package/dist/cli/run.js +28 -4
  42. package/dist/cli/run.js.map +1 -1
  43. package/dist/cli/types.d.ts +9 -0
  44. package/dist/cli/types.js +77 -0
  45. package/dist/cli/types.js.map +1 -0
  46. package/dist/client/index.d.ts +10 -0
  47. package/dist/client/index.js +7 -0
  48. package/dist/client/index.js.map +1 -0
  49. package/dist/client/manifest.js +6 -2
  50. package/dist/client/manifest.js.map +1 -1
  51. package/dist/client/runner.js +37 -3
  52. package/dist/client/runner.js.map +1 -1
  53. package/dist/client/typed.d.ts +54 -0
  54. package/dist/client/typed.js +490 -0
  55. package/dist/client/typed.js.map +1 -0
  56. package/dist/client/types.d.ts +196 -0
  57. package/dist/client/types.js +2 -0
  58. package/dist/client/types.js.map +1 -0
  59. package/dist/command.d.ts +61 -5
  60. package/dist/command.js +42 -1
  61. package/dist/command.js.map +1 -1
  62. package/dist/config.d.ts +42 -10
  63. package/dist/config.js +32 -7
  64. package/dist/config.js.map +1 -1
  65. package/dist/errors.d.ts +21 -3
  66. package/dist/errors.js +13 -0
  67. package/dist/errors.js.map +1 -1
  68. package/dist/expose.d.ts +9 -0
  69. package/dist/expose.js +52 -0
  70. package/dist/expose.js.map +1 -0
  71. package/dist/index.d.ts +15 -6
  72. package/dist/index.js +7 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/inspect/agent.d.ts +104 -0
  75. package/dist/inspect/agent.js +125 -0
  76. package/dist/inspect/agent.js.map +1 -0
  77. package/dist/inspect/audit.d.ts +46 -0
  78. package/dist/inspect/audit.js +133 -0
  79. package/dist/inspect/audit.js.map +1 -0
  80. package/dist/inspect/compare.d.ts +12 -0
  81. package/dist/inspect/compare.js +31 -0
  82. package/dist/inspect/compare.js.map +1 -0
  83. package/dist/inspect/index.d.ts +17 -0
  84. package/dist/inspect/index.js +12 -0
  85. package/dist/inspect/index.js.map +1 -0
  86. package/dist/inspect/inspector.d.ts +168 -0
  87. package/dist/inspect/inspector.js +392 -0
  88. package/dist/inspect/inspector.js.map +1 -0
  89. package/dist/inspect/profiles.d.ts +95 -0
  90. package/dist/inspect/profiles.js +235 -0
  91. package/dist/inspect/profiles.js.map +1 -0
  92. package/dist/inspect/scenario.d.ts +126 -0
  93. package/dist/inspect/scenario.js +555 -0
  94. package/dist/inspect/scenario.js.map +1 -0
  95. package/dist/inspect/ui-server.d.ts +31 -0
  96. package/dist/inspect/ui-server.js +378 -0
  97. package/dist/inspect/ui-server.js.map +1 -0
  98. package/dist/inspect/ui.html +917 -0
  99. package/dist/instrumentation.d.ts +13 -2
  100. package/dist/instrumentation.js +11 -1
  101. package/dist/instrumentation.js.map +1 -1
  102. package/dist/invoke.d.ts +32 -5
  103. package/dist/invoke.js +150 -16
  104. package/dist/invoke.js.map +1 -1
  105. package/dist/manifest.d.ts +13 -2
  106. package/dist/manifest.js +5 -1
  107. package/dist/manifest.js.map +1 -1
  108. package/dist/mcp/constants.d.ts +5 -0
  109. package/dist/mcp/constants.js +5 -0
  110. package/dist/mcp/constants.js.map +1 -1
  111. package/dist/mcp/dispatch.d.ts +1 -1
  112. package/dist/mcp/dispatch.js +14 -3
  113. package/dist/mcp/dispatch.js.map +1 -1
  114. package/dist/mcp/help.d.ts +2 -0
  115. package/dist/mcp/help.js +6 -1
  116. package/dist/mcp/help.js.map +1 -1
  117. package/dist/mcp/http.d.ts +16 -2
  118. package/dist/mcp/http.js +76 -7
  119. package/dist/mcp/http.js.map +1 -1
  120. package/dist/mcp/index.d.ts +1 -1
  121. package/dist/mcp/index.js +1 -1
  122. package/dist/mcp/index.js.map +1 -1
  123. package/dist/mcp/server.d.ts +12 -1
  124. package/dist/mcp/server.js +63 -17
  125. package/dist/mcp/server.js.map +1 -1
  126. package/dist/mcp/stdio.d.ts +4 -0
  127. package/dist/mcp/stdio.js +3 -0
  128. package/dist/mcp/stdio.js.map +1 -1
  129. package/dist/mcp/tasks.d.ts +3 -1
  130. package/dist/mcp/tasks.js +6 -3
  131. package/dist/mcp/tasks.js.map +1 -1
  132. package/dist/principal.d.ts +22 -0
  133. package/dist/principal.js +29 -0
  134. package/dist/principal.js.map +1 -1
  135. package/dist/registry.d.ts +5 -0
  136. package/dist/registry.js +21 -0
  137. package/dist/registry.js.map +1 -1
  138. package/dist/remote/client.d.ts +2 -0
  139. package/dist/remote/client.js +3 -2
  140. package/dist/remote/client.js.map +1 -1
  141. package/dist/remote/oauth.d.ts +1 -1
  142. package/dist/remote/oauth.js +1 -1
  143. package/dist/remote/oauth.js.map +1 -1
  144. package/dist/start.js +6 -2
  145. package/dist/start.js.map +1 -1
  146. package/dist/suggest.js +1 -1
  147. package/dist/suggest.js.map +1 -1
  148. package/dist/typegen.d.ts +27 -0
  149. package/dist/typegen.js +62 -0
  150. package/dist/typegen.js.map +1 -0
  151. package/dist/version-constant.d.ts +5 -0
  152. package/dist/version-constant.js +6 -0
  153. package/dist/version-constant.js.map +1 -0
  154. package/dist/version.d.ts +1 -2
  155. package/dist/version.js +1 -2
  156. package/dist/version.js.map +1 -1
  157. package/package.json +12 -2
@@ -0,0 +1,555 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { CallError } from "../client/typed.js";
4
+ import { AGENT_TIMEOUT_MS, checkAudit, connectAgent, instructAgent } from "./agent.js";
5
+ import { isRecord, partial } from "./compare.js";
6
+ import { orderedBody, outcomeOf } from "./inspector.js";
7
+ import { findProfile, INSPECT_DIR, ProfileUnavailableError } from "./profiles.js";
8
+ /**
9
+ * 시나리오 (specs/inspector.md §4): 호출 순서 + 기대 결과를 적은 JSON 파일. 사람 · CLI · 웹 UI · 에이전트가 같은 파일을 쓴다.
10
+ * 코드 실행 없음 (값 비교만)
11
+ */
12
+ /** <프로젝트>/inspect/scenarios */
13
+ export const SCENARIOS_DIR = "scenarios";
14
+ /** agent 단계 결과의 as */
15
+ export const AGENT_AS = "agent";
16
+ /** 시나리오 파일 형식 오류. issues = "<위치>: <문제>" */
17
+ export class ScenarioFileError extends Error {
18
+ file;
19
+ issues;
20
+ constructor(file, issues) {
21
+ super(`${file}: invalid scenario file\n${issues.map((i) => ` ${i}`).join("\n")}`);
22
+ this.name = "ScenarioFileError";
23
+ this.file = file;
24
+ this.issues = issues;
25
+ }
26
+ }
27
+ /** 경로 · 폴더 → 시나리오 파일 목록 (폴더 = 그 안의 *.json, 이름 순). 경로 없음 = <cwd>/inspect/scenarios */
28
+ export function scenarioFiles(cwd, paths) {
29
+ const targets = paths.length > 0 ? paths.map((p) => resolve(cwd, p)) : [join(resolve(cwd), INSPECT_DIR, SCENARIOS_DIR)];
30
+ const files = [];
31
+ for (const target of targets) {
32
+ if (!existsSync(target))
33
+ throw new ScenarioFileError(target, ["file not found"]);
34
+ if (!statSync(target).isDirectory()) {
35
+ files.push(target);
36
+ continue;
37
+ }
38
+ const found = readdirSync(target)
39
+ .filter((name) => name.endsWith(".json"))
40
+ .sort()
41
+ .map((name) => join(target, name));
42
+ if (found.length === 0)
43
+ throw new ScenarioFileError(target, ["no *.json scenario files in this folder"]);
44
+ files.push(...found);
45
+ }
46
+ return files;
47
+ }
48
+ /** 파일 읽기 + 검증. profiles 를 주면 없는 프로필 이름도 오류 */
49
+ export function loadScenario(file, profiles) {
50
+ let value;
51
+ try {
52
+ value = JSON.parse(readFileSync(file, "utf8"));
53
+ }
54
+ catch (error) {
55
+ throw new ScenarioFileError(file, [`not valid JSON (${error instanceof Error ? error.message : String(error)})`]);
56
+ }
57
+ return parseScenario(value, file, profiles);
58
+ }
59
+ const SCENARIO_KEYS = ["name", "description", "as", "steps"];
60
+ const CALL_KEYS = ["name", "as", "call", "input", "confirmed", "answers", "decline", "expect", "save"];
61
+ const COMMANDS_KEYS = ["name", "as", "commands"];
62
+ const AGENT_KEYS = ["name", "agent", "from", "expect", "timeoutMs"];
63
+ /** 시나리오 내용 검증 (문제를 모두 모아 한 번에) */
64
+ export function parseScenario(value, file, profiles) {
65
+ if (!isRecord(value))
66
+ throw new ScenarioFileError(file, ['must be an object ({ "name": …, "steps": [ … ] })']);
67
+ const issues = [];
68
+ const unknownKeys = (at, object, allowed) => {
69
+ for (const key of Object.keys(object))
70
+ if (!allowed.includes(key))
71
+ issues.push(`${at}${key}: unknown key`);
72
+ };
73
+ const profileName = (at, as) => {
74
+ if (as === undefined)
75
+ return undefined;
76
+ if (typeof as !== "string" || as === "") {
77
+ issues.push(`${at}: must be a profile name`);
78
+ return undefined;
79
+ }
80
+ if (profiles && !findProfile(profiles, as)) {
81
+ issues.push(`${at}: unknown profile "${as}" (profiles: ${profiles.profiles.map((p) => p.name).join(", ")})`);
82
+ }
83
+ return as;
84
+ };
85
+ /** 감사 이벤트의 일부 목록 */
86
+ const eventPatterns = (at, list) => {
87
+ if (list === undefined)
88
+ return undefined;
89
+ if (!Array.isArray(list) || !list.every(isRecord)) {
90
+ issues.push(`${at}: must be an array of objects (parts of audit events)`);
91
+ return undefined;
92
+ }
93
+ return list;
94
+ };
95
+ unknownKeys("", value, SCENARIO_KEYS);
96
+ const scenario = { file, name: basename(file, ".json"), steps: [] };
97
+ if (value.name !== undefined) {
98
+ if (typeof value.name !== "string" || value.name === "")
99
+ issues.push("name: must be a non-empty string");
100
+ else
101
+ scenario.name = value.name;
102
+ }
103
+ if (value.description !== undefined) {
104
+ if (typeof value.description !== "string")
105
+ issues.push("description: must be a string");
106
+ else
107
+ scenario.description = value.description;
108
+ }
109
+ const as = profileName("as", value.as);
110
+ if (as !== undefined)
111
+ scenario.as = as;
112
+ if (!Array.isArray(value.steps) || value.steps.length === 0) {
113
+ issues.push("steps: must be a non-empty array");
114
+ }
115
+ else {
116
+ value.steps.forEach((raw, i) => {
117
+ const at = `steps[${i + 1}]`;
118
+ if (!isRecord(raw))
119
+ return void issues.push(`${at}: must be an object`);
120
+ const hasCall = raw.call !== undefined;
121
+ const hasCommands = raw.commands !== undefined;
122
+ const hasAgent = raw.agent !== undefined;
123
+ if (hasCall && hasCommands)
124
+ return void issues.push(`${at}: use either "call" or "commands", not both`);
125
+ if (hasAgent && (hasCall || hasCommands))
126
+ return void issues.push(`${at}: "agent" cannot be combined with "call" or "commands"`);
127
+ if (!hasCall && !hasCommands && !hasAgent)
128
+ return void issues.push(`${at}: needs "call" (a command ID), "commands", or "agent" (an instruction)`);
129
+ const before = issues.length;
130
+ if (hasAgent) {
131
+ unknownKeys(`${at}.`, raw, AGENT_KEYS);
132
+ const step = { agent: "" };
133
+ if (raw.name !== undefined) {
134
+ if (typeof raw.name !== "string")
135
+ issues.push(`${at}.name: must be a string`);
136
+ else
137
+ step.name = raw.name;
138
+ }
139
+ if (typeof raw.agent !== "string" || raw.agent.trim() === "")
140
+ issues.push(`${at}.agent: must be the instruction text`);
141
+ else
142
+ step.agent = raw.agent;
143
+ if (raw.from !== undefined) {
144
+ if (!isRecord(raw.from))
145
+ issues.push(`${at}.from: must be an object (part of an audit event)`);
146
+ else
147
+ step.from = raw.from;
148
+ }
149
+ if (raw.timeoutMs !== undefined) {
150
+ if (typeof raw.timeoutMs !== "number" || !Number.isFinite(raw.timeoutMs) || raw.timeoutMs <= 0)
151
+ issues.push(`${at}.timeoutMs: must be a positive number`);
152
+ else
153
+ step.timeoutMs = raw.timeoutMs;
154
+ }
155
+ if (raw.expect !== undefined) {
156
+ const e = raw.expect;
157
+ if (!isRecord(e))
158
+ issues.push(`${at}.expect: must be { "audit"?: [ … ], "none"?: [ … ] }`);
159
+ else {
160
+ unknownKeys(`${at}.expect.`, e, ["audit", "none"]);
161
+ const expect = {};
162
+ for (const key of ["audit", "none"]) {
163
+ const list = eventPatterns(`${at}.expect.${key}`, e[key]);
164
+ if (list)
165
+ expect[key] = list;
166
+ }
167
+ step.expect = expect;
168
+ }
169
+ }
170
+ if (issues.length === before)
171
+ scenario.steps.push(step);
172
+ return;
173
+ }
174
+ unknownKeys(`${at}.`, raw, hasCall ? CALL_KEYS : COMMANDS_KEYS);
175
+ if (raw.name !== undefined && typeof raw.name !== "string")
176
+ issues.push(`${at}.name: must be a string`);
177
+ const stepAs = profileName(`${at}.as`, raw.as);
178
+ const common = { ...(typeof raw.name === "string" ? { name: raw.name } : {}), ...(stepAs !== undefined ? { as: stepAs } : {}) };
179
+ if (hasCommands) {
180
+ const c = raw.commands;
181
+ const list = (key) => {
182
+ const v = isRecord(c) ? c[key] : undefined;
183
+ if (v === undefined)
184
+ return undefined;
185
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
186
+ issues.push(`${at}.commands.${key}: must be an array of command IDs`);
187
+ return undefined;
188
+ }
189
+ return v;
190
+ };
191
+ if (!isRecord(c))
192
+ issues.push(`${at}.commands: must be { "visible"?: [ids], "hidden"?: [ids] }`);
193
+ else {
194
+ unknownKeys(`${at}.commands.`, c, ["visible", "hidden"]);
195
+ if (c.visible === undefined && c.hidden === undefined)
196
+ issues.push(`${at}.commands: needs "visible" or "hidden"`);
197
+ }
198
+ const visible = list("visible");
199
+ const hidden = list("hidden");
200
+ if (issues.length === before) {
201
+ scenario.steps.push({ ...common, commands: { ...(visible ? { visible } : {}), ...(hidden ? { hidden } : {}) } });
202
+ }
203
+ return;
204
+ }
205
+ const step = { ...common, call: "" };
206
+ if (typeof raw.call !== "string" || raw.call === "")
207
+ issues.push(`${at}.call: must be a command ID`);
208
+ else
209
+ step.call = raw.call;
210
+ if (raw.input !== undefined) {
211
+ if (!isRecord(raw.input))
212
+ issues.push(`${at}.input: must be an object`);
213
+ else
214
+ step.input = raw.input;
215
+ }
216
+ for (const key of ["confirmed", "decline"]) {
217
+ if (raw[key] === undefined)
218
+ continue;
219
+ if (typeof raw[key] !== "boolean")
220
+ issues.push(`${at}.${key}: must be true or false`);
221
+ else if (raw[key])
222
+ step[key] = true;
223
+ }
224
+ if (raw.answers !== undefined) {
225
+ const a = raw.answers;
226
+ if (!isRecord(a))
227
+ issues.push(`${at}.answers: must be { "confirm"?: boolean, "form"?: object, "url"?: boolean }`);
228
+ else {
229
+ unknownKeys(`${at}.answers.`, a, ["confirm", "form", "url"]);
230
+ const answers = {};
231
+ if (a.confirm !== undefined) {
232
+ if (typeof a.confirm !== "boolean")
233
+ issues.push(`${at}.answers.confirm: must be true or false`);
234
+ else
235
+ answers.confirm = a.confirm;
236
+ }
237
+ if (a.url !== undefined) {
238
+ if (typeof a.url !== "boolean")
239
+ issues.push(`${at}.answers.url: must be true or false`);
240
+ else
241
+ answers.url = a.url;
242
+ }
243
+ if (a.form !== undefined) {
244
+ if (a.form !== false && !isRecord(a.form))
245
+ issues.push(`${at}.answers.form: must be an object (the values to send) or false`);
246
+ else
247
+ answers.form = a.form;
248
+ }
249
+ step.answers = answers;
250
+ }
251
+ }
252
+ if (raw.expect !== undefined) {
253
+ const e = raw.expect;
254
+ if (!isRecord(e))
255
+ issues.push(`${at}.expect: must be { "ok"?: boolean, "value"?: …, "error"?: code | { … } }`);
256
+ else {
257
+ unknownKeys(`${at}.expect.`, e, ["ok", "value", "error", "audit"]);
258
+ const expect = {};
259
+ const audit = eventPatterns(`${at}.expect.audit`, e.audit);
260
+ if (audit)
261
+ expect.audit = audit;
262
+ if (e.ok !== undefined) {
263
+ if (typeof e.ok !== "boolean")
264
+ issues.push(`${at}.expect.ok: must be true or false`);
265
+ else
266
+ expect.ok = e.ok;
267
+ }
268
+ if (e.value !== undefined)
269
+ expect.value = e.value;
270
+ if (e.error !== undefined) {
271
+ if (typeof e.error !== "string" && !isRecord(e.error))
272
+ issues.push(`${at}.expect.error: must be an error code or an object`);
273
+ else
274
+ expect.error = e.error;
275
+ }
276
+ if (e.error !== undefined && (e.ok === true || e.value !== undefined)) {
277
+ issues.push(`${at}.expect: "error" cannot be combined with "ok": true or "value"`);
278
+ }
279
+ if (e.ok === false && e.value !== undefined)
280
+ issues.push(`${at}.expect: "value" cannot be combined with "ok": false`);
281
+ step.expect = expect;
282
+ }
283
+ }
284
+ if (raw.save !== undefined) {
285
+ if (!isRecord(raw.save) || !Object.values(raw.save).every((v) => typeof v === "string")) {
286
+ issues.push(`${at}.save: must be { "<variable>": "<path in the result>" }`);
287
+ }
288
+ else
289
+ step.save = raw.save;
290
+ }
291
+ if (issues.length === before)
292
+ scenario.steps.push(step);
293
+ });
294
+ }
295
+ if (issues.length > 0)
296
+ throw new ScenarioFileError(file, issues);
297
+ return scenario;
298
+ }
299
+ /** 단계 순서대로 실행. 실패한 단계가 있어도 끝까지 (bail 제외) */
300
+ export async function runScenario(inspector, scenario, options = {}) {
301
+ const started = Date.now();
302
+ const vars = new Map();
303
+ const steps = [];
304
+ for (const [i, step] of scenario.steps.entries()) {
305
+ if (options.signal?.aborted)
306
+ break;
307
+ const as = "agent" in step ? AGENT_AS : inspector.profile(step.as ?? scenario.as).name;
308
+ const name = step.name ?? defaultName(step, as);
309
+ options.onStepStart?.({ step: i + 1, name, as });
310
+ const result = "agent" in step
311
+ ? await runAgent(inspector, step, vars, options)
312
+ : "call" in step
313
+ ? await runCall(inspector, step, as, vars, options.signal)
314
+ : await runCommands(inspector, step, as);
315
+ const full = { step: i + 1, name, as, ...result, pass: result.failures.length === 0 };
316
+ steps.push(full);
317
+ options.onStep?.(full);
318
+ if (!full.pass && options.bail === true)
319
+ break;
320
+ }
321
+ const result = {
322
+ name: scenario.name,
323
+ pass: steps.length === scenario.steps.length && steps.every((s) => s.pass),
324
+ durationMs: Date.now() - started,
325
+ steps,
326
+ };
327
+ if (scenario.file !== undefined)
328
+ result.file = scenario.file;
329
+ return result;
330
+ }
331
+ async function runCall(inspector, step, as, vars, signal) {
332
+ const failures = [];
333
+ const missing = [];
334
+ const input = substitute(step.input ?? {}, vars, missing);
335
+ if (missing.length > 0) {
336
+ for (const name of missing)
337
+ failures.push({ path: "input", expected: `{{${name}}}`, actual: undefined });
338
+ return { durationMs: 0, outcome: undefined, failures };
339
+ }
340
+ const record = await inspector.call(step.call, input, {
341
+ profile: as,
342
+ ...(step.confirmed ? { confirmed: true } : {}),
343
+ ...(step.answers ? { answers: step.answers } : {}),
344
+ ...(step.decline ? { decline: true } : {}),
345
+ ...(signal ? { signal } : {}),
346
+ });
347
+ const outcome = outcomeOf(record);
348
+ compare(step.expect, outcome, failures);
349
+ if (step.expect?.audit) {
350
+ if (!inspector.auditConnected)
351
+ failures.push({ path: "audit", expected: "an audit source", actual: AUDIT_UNAVAILABLE });
352
+ else
353
+ partial(step.expect.audit, await auditOf(inspector, record, step.expect.audit.length, signal), "audit", failures);
354
+ }
355
+ for (const [name, path] of Object.entries(step.save ?? {})) {
356
+ const value = outcome.ok ? valueAt(outcome.value, path) : undefined;
357
+ if (value === undefined)
358
+ failures.push({ path: `save.${name}`, expected: path, actual: undefined });
359
+ else
360
+ vars.set(name, value);
361
+ }
362
+ return { durationMs: record.durationMs, outcome, failures, record: record.id };
363
+ }
364
+ const AUDIT_UNAVAILABLE = "AUDIT_UNAVAILABLE: audit events are not available for this target (run against the mext dev server of this project, or pass an audit source to createInspector)";
365
+ const AGENT_UNAVAILABLE = "AGENT_UNAVAILABLE: no agent client (use mext inspect run --agent <module>, or runScenario(…, { agent }))";
366
+ /** 호출의 감사 이벤트가 응답보다 늦게 올 때 기다리는 시간 */
367
+ const CALL_AUDIT_WAIT_MS = 2_000;
368
+ /** 이 호출의 감사 이벤트 (trace ID 가 같은 것). 기대한 수만큼 올 때까지 잠깐 기다린다 */
369
+ async function auditOf(inspector, record, count, signal) {
370
+ const read = () => inspector.auditEvents().filter((event) => event.traceId === record.traceId);
371
+ const deadline = Date.now() + CALL_AUDIT_WAIT_MS;
372
+ while (read().length < count && Date.now() < deadline && signal?.aborted !== true)
373
+ await new Promise((resolve) => setTimeout(resolve, 20));
374
+ return read();
375
+ }
376
+ async function runAgent(inspector, step, vars, options) {
377
+ const started = Date.now();
378
+ const failures = [];
379
+ const missing = [];
380
+ const text = substitute(step.agent, vars, missing);
381
+ for (const name of missing)
382
+ failures.push({ path: "agent", expected: `{{${name}}}`, actual: undefined });
383
+ const agent = options.agent;
384
+ if (!agent)
385
+ failures.push({ path: "agent", expected: "an agent client", actual: AGENT_UNAVAILABLE });
386
+ if (!inspector.auditConnected)
387
+ failures.push({ path: "audit", expected: "an audit source", actual: AUDIT_UNAVAILABLE });
388
+ if (failures.length > 0 || !agent)
389
+ return { durationMs: 0, outcome: undefined, failures };
390
+ try {
391
+ await connectAgent(agent, options.signal);
392
+ }
393
+ catch (error) {
394
+ failures.push({ path: "agent", expected: "connected", actual: error instanceof Error ? error.message : String(error) });
395
+ return { durationMs: Date.now() - started, outcome: undefined, failures };
396
+ }
397
+ const observation = await instructAgent(inspector, agent, {
398
+ text,
399
+ timeoutMs: step.timeoutMs ?? AGENT_TIMEOUT_MS,
400
+ ...(step.from ? { from: step.from } : {}),
401
+ ...(options.agentSettleMs !== undefined ? { settleMs: options.agentSettleMs } : {}),
402
+ ...(options.signal ? { signal: options.signal } : {}),
403
+ });
404
+ if (!observation.ok)
405
+ failures.push({ path: "agent", expected: "idle", actual: observation.problem });
406
+ checkAudit(step.expect, observation.events, failures);
407
+ return { durationMs: Date.now() - started, outcome: observation, failures };
408
+ }
409
+ async function runCommands(inspector, step, as) {
410
+ const started = Date.now();
411
+ const failures = [];
412
+ let ids;
413
+ try {
414
+ ids = (await inspector.commands(as)).commands.map((c) => c.id);
415
+ }
416
+ catch (error) {
417
+ const body = listErrorBody(error);
418
+ failures.push({ path: "commands", expected: "a command list", actual: body });
419
+ return { durationMs: Date.now() - started, outcome: { ok: false, error: body }, failures };
420
+ }
421
+ const notVisible = (step.commands.visible ?? []).filter((id) => !ids.includes(id));
422
+ const notHidden = (step.commands.hidden ?? []).filter((id) => ids.includes(id));
423
+ if (notVisible.length > 0)
424
+ failures.push({ path: "commands.visible", expected: notVisible, actual: "hidden" });
425
+ if (notHidden.length > 0)
426
+ failures.push({ path: "commands.hidden", expected: notHidden, actual: "visible" });
427
+ return { durationMs: Date.now() - started, outcome: { ok: true, commands: ids }, failures };
428
+ }
429
+ /** 목록 조회 실패 → 오류 본문 */
430
+ export function listErrorBody(error) {
431
+ if (error instanceof CallError)
432
+ return orderedBody(error);
433
+ if (error instanceof ProfileUnavailableError) {
434
+ return { code: error.code, message: error.message, profile: error.profile, missing: [...error.missing] };
435
+ }
436
+ return { code: "CLIENT_ERROR", message: error instanceof Error ? error.message : String(error) };
437
+ }
438
+ function defaultName(step, as) {
439
+ if ("agent" in step)
440
+ return `agent: ${step.agent.length > 60 ? `${step.agent.slice(0, 57)}…` : step.agent}`;
441
+ return "call" in step ? `${step.call} as ${as}` : `commands as ${as}`;
442
+ }
443
+ /** 기대 결과 비교 (expect 없음 = 통과) */
444
+ export function compare(expect, outcome, failures) {
445
+ if (!expect)
446
+ return;
447
+ const wantsError = expect.error !== undefined || expect.ok === false;
448
+ const wantsOk = expect.ok === true || expect.value !== undefined;
449
+ if (wantsOk && !outcome.ok) {
450
+ failures.push({ path: "ok", expected: true, actual: false });
451
+ return;
452
+ }
453
+ if (wantsError && outcome.ok) {
454
+ failures.push({ path: "ok", expected: false, actual: true });
455
+ return;
456
+ }
457
+ if (outcome.ok) {
458
+ if (expect.value !== undefined)
459
+ partial(expect.value, outcome.value, "value", failures);
460
+ return;
461
+ }
462
+ if (typeof expect.error === "string") {
463
+ if (outcome.error.code !== expect.error)
464
+ failures.push({ path: "error.code", expected: expect.error, actual: outcome.error.code });
465
+ }
466
+ else if (expect.error !== undefined)
467
+ partial(expect.error, outcome.error, "error", failures);
468
+ }
469
+ const WHOLE = /^\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$/;
470
+ const PART = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
471
+ /** {{변수}}: 문자열 전체 = 값 그대로 (타입 유지) · 일부 = 문자열로. 없는 변수 = missing 에 이름 */
472
+ function substitute(value, vars, missing) {
473
+ if (typeof value === "string") {
474
+ const whole = WHOLE.exec(value);
475
+ const lookup = (name) => {
476
+ if (!vars.has(name) && !missing.includes(name))
477
+ missing.push(name);
478
+ return vars.get(name);
479
+ };
480
+ if (whole)
481
+ return lookup(whole[1]);
482
+ return value.replace(PART, (_, name) => {
483
+ const v = lookup(name);
484
+ return typeof v === "string" ? v : JSON.stringify(v ?? null);
485
+ });
486
+ }
487
+ if (Array.isArray(value))
488
+ return value.map((v) => substitute(v, vars, missing));
489
+ if (isRecord(value))
490
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substitute(v, vars, missing)]));
491
+ return value;
492
+ }
493
+ /** 점 구분 경로 (items.0.id) */
494
+ function valueAt(value, path) {
495
+ let current = value;
496
+ for (const key of path === "" ? [] : path.split(".")) {
497
+ if (Array.isArray(current))
498
+ current = current[Number(key)];
499
+ else if (isRecord(current))
500
+ current = current[key];
501
+ else
502
+ return undefined;
503
+ }
504
+ return current;
505
+ }
506
+ /** 호출 기록 → 시나리오 단계 (관찰한 결과를 기대 결과로). 오류 = code · reason 만 */
507
+ export function stepFromRecord(record, options = {}) {
508
+ const step = { as: record.profile, call: record.command };
509
+ if (Object.keys(record.input).length > 0)
510
+ step.input = record.input;
511
+ if (record.confirmed)
512
+ step.confirmed = true;
513
+ const answers = options.answers ?? (options.decline ? undefined : answersOf(record));
514
+ if (answers)
515
+ step.answers = answers;
516
+ if (options.decline)
517
+ step.decline = true;
518
+ if (record.ok)
519
+ step.expect = record.value === undefined ? { ok: true } : { ok: true, value: record.value };
520
+ else {
521
+ const error = record.error;
522
+ step.expect = { error: typeof error?.reason === "string" ? { code: error.code, reason: error.reason } : (error?.code ?? "UNKNOWN") };
523
+ }
524
+ return step;
525
+ }
526
+ /** 기록의 입력 요청에 보낸 답 → 시나리오 answers (종류별 마지막 답). 답한 요청이 없으면 undefined */
527
+ function answersOf(record) {
528
+ const answers = {};
529
+ for (const { kind, answer } of record.inputs) {
530
+ if (answer === undefined)
531
+ continue;
532
+ const accepted = answer === true || (typeof answer === "object" && answer.action === "accept");
533
+ if (kind === "form")
534
+ answers.form = accepted ? ((typeof answer === "object" && "content" in answer ? answer.content : undefined) ?? {}) : false;
535
+ else
536
+ answers[kind] = accepted;
537
+ }
538
+ return Object.keys(answers).length > 0 ? answers : undefined;
539
+ }
540
+ /** 시나리오 파일 끝에 단계를 덧붙인다 (파일이 없으면 만든다). 반환 = 단계 수 */
541
+ export function appendStep(file, step) {
542
+ let content;
543
+ if (existsSync(file)) {
544
+ loadScenario(file); // 형식 확인
545
+ content = JSON.parse(readFileSync(file, "utf8"));
546
+ content.steps = [...content.steps, step];
547
+ }
548
+ else {
549
+ mkdirSync(dirname(file), { recursive: true });
550
+ content = { name: basename(file, ".json"), steps: [step] };
551
+ }
552
+ writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`);
553
+ return content.steps.length;
554
+ }
555
+ //# sourceMappingURL=scenario.js.map