waygraph 0.16.0 → 0.16.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 (143) hide show
  1. package/dist/cli/commands/browser.d.ts +2 -0
  2. package/dist/cli/commands/browser.js +109 -0
  3. package/dist/cli/commands/codemod-stubs.js +90 -2
  4. package/dist/cli/commands/pilot.d.ts +2 -0
  5. package/dist/cli/commands/pilot.js +102 -0
  6. package/dist/cli/commands/session-ops.d.ts +3 -0
  7. package/dist/cli/commands/session-ops.js +257 -0
  8. package/dist/cli/commands/session.js +20 -461
  9. package/dist/cli/run-chain.js +2 -2
  10. package/dist/cli/usage.d.ts +1 -1
  11. package/dist/cli/util.js +1 -1
  12. package/dist/demo/narration/demo-pace.d.ts +2 -2
  13. package/dist/demo/narration/demo-pace.js +1 -1
  14. package/dist/demo/narration/device.d.ts +2 -2
  15. package/dist/demo/narration/merge.d.ts +5 -3
  16. package/dist/demo/narration/merge.js +3 -7
  17. package/dist/demo/narration/narration.d.ts +9 -13
  18. package/dist/demo/narration/narration.js +5 -17
  19. package/dist/demo/narration/stub-ctx.js +3 -3
  20. package/dist/demo/narration/todo-dock.d.ts +5 -5
  21. package/dist/demo/overlay/cursor.d.ts +9 -2
  22. package/dist/demo/overlay/cursor.js +1 -1
  23. package/dist/demo/overlay/device-stage.d.ts +15 -2
  24. package/dist/demo/overlay/inpage/banner.d.ts +2 -1
  25. package/dist/demo/overlay/inpage/banner.js +3 -3
  26. package/dist/demo/overlay/inpage/caption.d.ts +1 -1
  27. package/dist/demo/overlay/inpage/core.d.ts +2 -1
  28. package/dist/demo/overlay/inpage/core.js +1 -7
  29. package/dist/demo/overlay/inpage/device-chip.d.ts +1 -1
  30. package/dist/demo/overlay/inpage/device-chip.js +6 -6
  31. package/dist/demo/overlay/inpage/network.d.ts +3 -2
  32. package/dist/demo/overlay/inpage/network.js +1 -12
  33. package/dist/demo/overlay/inpage/panel-chrome.d.ts +1 -1
  34. package/dist/demo/overlay/inpage/panel-chrome.js +1 -1
  35. package/dist/demo/overlay/inpage/stamp.d.ts +1 -1
  36. package/dist/demo/overlay/inpage/stamp.js +1 -1
  37. package/dist/demo/overlay/inpage/swipe-zoom.d.ts +1 -1
  38. package/dist/demo/overlay/inpage/swipe-zoom.js +1 -1
  39. package/dist/demo/overlay/inpage/todos.d.ts +2 -1
  40. package/dist/demo/overlay/inpage/todos.js +1 -2
  41. package/dist/demo/overlay/network-capture.d.ts +2 -2
  42. package/dist/demo/overlay/network-capture.js +1 -1
  43. package/dist/demo/overlay/overlay-css.js +1 -55
  44. package/dist/demo/overlay/overlay-stage.d.ts +19 -11
  45. package/dist/demo/overlay/overlay.d.ts +2 -2
  46. package/dist/demo/overlay/overlay.js +3 -2
  47. package/dist/demo/overlay/rings.d.ts +38 -18
  48. package/dist/demo/overlay/rings.js +21 -62
  49. package/dist/demo/overlay/ui/overlay.css +40 -0
  50. package/dist/demo/stepper/after.js +11 -34
  51. package/dist/demo/stepper/banner-log.d.ts +4 -6
  52. package/dist/demo/stepper/banner-log.js +3 -1
  53. package/dist/demo/stepper/before.js +6 -4
  54. package/dist/demo/stepper/demo-log.d.ts +26 -9
  55. package/dist/demo/stepper/demo-log.js +10 -27
  56. package/dist/demo/stepper/interaction-patch.d.ts +3 -2
  57. package/dist/demo/stepper/interaction-patch.js +22 -34
  58. package/dist/demo/stepper/panel-after.d.ts +17 -0
  59. package/dist/demo/stepper/panel-after.js +166 -0
  60. package/dist/demo/stepper/panel-before.d.ts +12 -0
  61. package/dist/demo/stepper/panel-before.js +241 -0
  62. package/dist/demo/stepper/slides.d.ts +16 -1
  63. package/dist/demo/stepper/slides.js +21 -5
  64. package/dist/demo/stepper/step-panels.d.ts +57 -9
  65. package/dist/demo/stepper/step-panels.js +10 -427
  66. package/dist/demo/stepper/stepper.js +1 -1
  67. package/dist/demo/stepper/teardown.d.ts +4 -5
  68. package/dist/demo/stepper/teardown.js +4 -6
  69. package/dist/demo/stepper/todo-advance.d.ts +3 -2
  70. package/dist/demo/stepper/todo-advance.js +2 -2
  71. package/dist/demo/stepper/view.d.ts +4 -14
  72. package/dist/demo/types.d.ts +69 -1
  73. package/dist/demo/verbose-log.js +1 -1
  74. package/dist/engine/block/action-page.d.ts +53 -0
  75. package/dist/engine/block/action-page.js +1 -0
  76. package/dist/engine/block/block.d.ts +24 -90
  77. package/dist/engine/block/block.js +2 -77
  78. package/dist/engine/block/branch.d.ts +17 -0
  79. package/dist/engine/block/branch.js +40 -0
  80. package/dist/engine/block/dialog.d.ts +13 -0
  81. package/dist/engine/block/dialog.js +37 -0
  82. package/dist/engine/block/kinds/method.d.ts +0 -2
  83. package/dist/engine/block/kinds/method.js +0 -1
  84. package/dist/engine/block/kinds/nav.js +1 -1
  85. package/dist/engine/flow/map-branch.d.ts +9 -0
  86. package/dist/engine/flow/map-branch.js +57 -0
  87. package/dist/engine/flow/map-builder.d.ts +7 -22
  88. package/dist/engine/flow/map-builder.js +6 -145
  89. package/dist/engine/flow/map-steps.d.ts +30 -0
  90. package/dist/engine/flow/map-steps.js +87 -0
  91. package/dist/engine/run/run.d.ts +1 -19
  92. package/dist/engine/run/run.js +0 -88
  93. package/dist/engine/stub/ring.d.ts +2 -4
  94. package/dist/engine/stub/ring.js +1 -0
  95. package/dist/engine/stub/stub.d.ts +16 -8
  96. package/dist/engine/trait/trait.js +1 -1
  97. package/dist/index.d.ts +15 -12
  98. package/dist/index.js +9 -7
  99. package/dist/live/auto/auto.d.ts +3 -15
  100. package/dist/live/auto/auto.js +10 -420
  101. package/dist/live/auto/explore.d.ts +2 -2
  102. package/dist/live/auto/mem.d.ts +11 -0
  103. package/dist/live/auto/mem.js +85 -0
  104. package/dist/live/auto/menu.d.ts +30 -0
  105. package/dist/live/auto/menu.js +335 -0
  106. package/dist/live/auto-session/auto-session.d.ts +23 -103
  107. package/dist/live/auto-session/auto-session.js +100 -370
  108. package/dist/live/auto-session/console.d.ts +4 -0
  109. package/dist/live/auto-session/console.js +41 -0
  110. package/dist/live/auto-session/files.d.ts +44 -0
  111. package/dist/live/auto-session/files.js +145 -0
  112. package/dist/live/auto-session/helpers.d.ts +5 -2
  113. package/dist/live/auto-session/helpers.js +17 -0
  114. package/dist/live/auto-session/inspect.d.ts +4 -0
  115. package/dist/live/auto-session/inspect.js +75 -0
  116. package/dist/live/auto-session/ipc.d.ts +3 -22
  117. package/dist/live/auto-session/ipc.js +7 -147
  118. package/dist/live/auto-session/open-browser.d.ts +6 -0
  119. package/dist/live/auto-session/open-browser.js +47 -0
  120. package/dist/live/pilot/pilot.d.ts +1 -1
  121. package/dist/live/pilot/pilot.js +2 -1
  122. package/dist/live/traverse/traverse.js +6 -200
  123. package/dist/live/traverse/worker.d.ts +37 -0
  124. package/dist/live/traverse/worker.js +200 -0
  125. package/dist/runner/plan/find.js +2 -2
  126. package/dist/runner/plan/plan.js +1 -1
  127. package/dist/runner/plan/seed-mem.js +5 -5
  128. package/dist/runner/plan/spec.js +1 -1
  129. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  130. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
  131. package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
  132. package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
  133. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
  134. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  135. package/package.json +5 -1
  136. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  137. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
  138. package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
  139. package/templates/quickstart/src/flows/shop.flow.ts +2 -1
  140. package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
  141. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  142. package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
  143. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
@@ -1,207 +1,13 @@
1
1
  import { Engine } from "../../engine/engine.js";
2
- import { buildFlow } from "../../engine/flow/flow.js";
2
+ import { envFlag, envString } from "../../engine/env.js";
3
3
  import { MemPage } from "../../engine/mem/mem.js";
4
- import { buildExploreContext, buildExploreMenu, } from "../auto/explore.js";
5
- import { detectHere, memStubValue } from "../auto/auto.js";
4
+ import { ignorePageGone, ignoreTimeout } from "../../engine/page-gone.js";
5
+ import { chromiumExecutablePath } from "../../runtime/browser.js";
6
6
  import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
7
- import { EdgeLeaseCoordinator } from "./lease.js";
7
+ import { buildExploreContext, } from "../auto/explore.js";
8
8
  import { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, writeCoverageReport, } from "./coverage.js";
9
- import { chromiumExecutablePath } from "../../runtime/browser.js";
10
- import { envFlag, envString } from "../../engine/env.js";
11
- import { ignorePageGone, ignoreTimeout } from "../../engine/page-gone.js";
12
- function seedMem(library, mem, dataJson) {
13
- const raw = dataJson ?? envString("WAYGRAPH_DATA") ?? envString("WAYGRAPH_AUTO_MEM");
14
- let envParsed = {};
15
- if (raw) {
16
- try {
17
- envParsed = JSON.parse(raw);
18
- }
19
- catch {
20
- throw new Error(`waygraph traverse: --data / WAYGRAPH_DATA is not valid JSON`);
21
- }
22
- }
23
- for (const entry of library.values()) {
24
- for (const k of entry.block.requires ?? []) {
25
- if (mem.has(k))
26
- continue;
27
- if (k.name in envParsed) {
28
- mem.set(k, envParsed[k.name]);
29
- continue;
30
- }
31
- const def = memStubValue(k);
32
- if (def !== undefined)
33
- mem.set(k, def);
34
- }
35
- }
36
- }
37
- function snapshotMem(library, mem) {
38
- const out = {};
39
- for (const entry of library.values()) {
40
- for (const k of entry.block.requires ?? []) {
41
- if (mem.has(k) && !(k.name in out)) {
42
- out[k.name] = mem.get(k);
43
- }
44
- }
45
- }
46
- return out;
47
- }
48
- function restoreMem(library, mem, snap, dataJson) {
49
- seedMem(library, mem, dataJson);
50
- for (const entry of library.values()) {
51
- for (const k of entry.block.requires ?? []) {
52
- if (k.name in snap) {
53
- mem.set(k, snap[k.name]);
54
- }
55
- }
56
- }
57
- }
58
- function missingMemKeys(entry, mem) {
59
- return (entry.block.requires ?? []).filter((k) => !mem.has(k));
60
- }
61
- function ensureMem(entry, mem) {
62
- const missing = missingMemKeys(entry, mem);
63
- if (missing.length === 0)
64
- return;
65
- throw new Error(`Block "${entry.block.name}" needs Mem keys: ${missing.map((k) => k.name).join(", ")} ` +
66
- `(pass --data '{...}' or WAYGRAPH_DATA)`);
67
- }
68
- function edgeKey(edge) {
69
- const id = edge.instanceOption?.id;
70
- return id ? `${edge.block}::${id}` : edge.block;
71
- }
72
- async function runOneBlock(engine, entry, context, page, mem) {
73
- const flow = buildFlow([entry.block], engine.config);
74
- const BLOCK_TIMEOUT_MS = 45_000;
75
- let timer;
76
- try {
77
- const ran = await Promise.race([
78
- flow.run(context, mem, { page, closeOnFinish: false }),
79
- new Promise((_, reject) => {
80
- timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s`)), BLOCK_TIMEOUT_MS);
81
- }),
82
- ]);
83
- return ran.result;
84
- }
85
- finally {
86
- if (timer)
87
- clearTimeout(timer);
88
- }
89
- }
90
- async function runTraverseWorker(opts) {
91
- const { traverseId, workerIndex, parallel, maxSteps, maxVisitsPerNode, maxVisitsPerEdge, timeoutMs, graph, library, engine, context, mem, leases, } = opts;
92
- let page = opts.page;
93
- const startUrl = opts.startUrl;
94
- const nodeVisits = new Map();
95
- const edgeVisits = new Map();
96
- const edgesHit = new Set();
97
- let steps = 0;
98
- const startedAt = Date.now();
99
- let lastHere = opts.from ?? null;
100
- let exitCode = 0;
101
- let leaf;
102
- try {
103
- for (;;) {
104
- if (Date.now() - startedAt > timeoutMs) {
105
- console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
106
- exitCode = 1;
107
- break;
108
- }
109
- if (steps >= maxSteps) {
110
- console.log(`[Killed max-steps[${traverseId}] node=${lastHere ?? "?"} steps=${steps} max=${maxSteps}]`);
111
- exitCode = 1;
112
- break;
113
- }
114
- if (page.isClosed()) {
115
- page = await context.newPage();
116
- if (startUrl) {
117
- await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(ignorePageGone);
118
- }
119
- }
120
- const detected = await detectHere(page, library.navBlocks);
121
- const here = lastHere ?? detected ?? opts.from ?? null;
122
- if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
123
- console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
124
- exitCode = 1;
125
- break;
126
- }
127
- const menu = await buildExploreMenu(page, graph, library, here);
128
- const candidates = menu.flat.filter((edge) => {
129
- const key = edgeKey(edge);
130
- if ((edgeVisits.get(key) ?? 0) >= maxVisitsPerEdge)
131
- return false;
132
- if (parallel > 1) {
133
- if (EdgeLeaseCoordinator.partitionIndex(key, parallel) !== workerIndex) {
134
- return false;
135
- }
136
- }
137
- return true;
138
- });
139
- if (candidates.length === 0) {
140
- const label = here ?? detected ?? "unknown";
141
- leaf = label;
142
- console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
143
- console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
144
- break;
145
- }
146
- const pick = [...candidates].sort((a, b) => {
147
- const aWild = a.from === "*" ? 1 : 0;
148
- const bWild = b.from === "*" ? 1 : 0;
149
- if (aWild !== bWild)
150
- return aWild - bWild;
151
- return edgeKey(a).localeCompare(edgeKey(b));
152
- })[0];
153
- const key = edgeKey(pick);
154
- if (!leases.tryClaim(key, traverseId)) {
155
- // Another worker won a race (instance fan-out); treat as unavailable.
156
- edgeVisits.set(key, maxVisitsPerEdge);
157
- continue;
158
- }
159
- const entry = library.byName.get(pick.block);
160
- if (!entry) {
161
- console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
162
- exitCode = 1;
163
- break;
164
- }
165
- const fromLabel = here ?? pick.from;
166
- try {
167
- if (pick.instanceOption) {
168
- mem.set(pick.instanceOption.key, pick.instanceOption.value);
169
- }
170
- ensureMem(entry, mem);
171
- console.error(`waygraph traverse[${traverseId}]: step ${steps + 1} ${fromLabel} -[${pick.block}]-> ${pick.to}` +
172
- (pick.label ? ` (${pick.label})` : ""));
173
- const out = await runOneBlock(engine, entry, context, page, mem);
174
- const observed = typeof out === "object" && out && "__state" in out
175
- ? String(out.__state)
176
- : pick.to;
177
- edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
178
- edgesHit.add(key);
179
- if (here)
180
- nodeVisits.set(here, (nodeVisits.get(here) ?? 0) + 1);
181
- lastHere = observed;
182
- steps += 1;
183
- if (observed !== pick.to && pick.to !== "*") {
184
- console.error(`waygraph traverse[${traverseId}]: WARN static to=${pick.to} observed=${observed}`);
185
- }
186
- }
187
- catch (err) {
188
- const msg = err instanceof Error ? err.message : String(err);
189
- console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${fromLabel} to=${pick.to}] ${msg}`);
190
- exitCode = 1;
191
- break;
192
- }
193
- }
194
- }
195
- finally {
196
- console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
197
- }
198
- return {
199
- exitCode,
200
- steps,
201
- edgesHit: [...edgesHit],
202
- ...(leaf ? { leaf } : {}),
203
- };
204
- }
9
+ import { EdgeLeaseCoordinator } from "./lease.js";
10
+ import { restoreMem, runTraverseWorker, seedMem, snapshotMem } from "./worker.js";
205
11
  /**
206
12
  * Serial or parallel graph crawl. Prints greppable PASS/FAIL + Coverage lines.
207
13
  * Exit: 0 leaf ok (+ coverage ok), 1 break/timeout, 2 coverage gate fail (Phase E).
@@ -0,0 +1,37 @@
1
+ import type { BrowserContext, Page } from "@playwright/test";
2
+ import type { WaygraphGraph } from "../../analysis/graph.js";
3
+ import type { Checkpoint } from "../../engine/checkpoint/checkpoint.js";
4
+ import { Engine } from "../../engine/engine.js";
5
+ import { MemPage, type MemKey } from "../../engine/mem/mem.js";
6
+ import type { BlockEntry } from "../../manifest/project.js";
7
+ import { buildExploreContext, type ExploreEdge } from "../auto/explore.js";
8
+ import { EdgeLeaseCoordinator } from "./lease.js";
9
+ import type { TraverseWorkerResult } from "./traverse.js";
10
+ export declare function seedMem(library: Map<string, BlockEntry>, mem: MemPage, dataJson?: string): void;
11
+ export declare function snapshotMem(library: Map<string, BlockEntry>, mem: MemPage): Record<string, unknown>;
12
+ export declare function restoreMem(library: Map<string, BlockEntry>, mem: MemPage, snap: Record<string, unknown>, dataJson?: string): void;
13
+ export declare function missingMemKeys(entry: BlockEntry, mem: MemPage): MemKey<unknown>[];
14
+ export declare function ensureMem(entry: BlockEntry, mem: MemPage): void;
15
+ export declare function edgeKey(edge: ExploreEdge): string;
16
+ export declare function runOneBlock(engine: Engine, entry: BlockEntry, context: BrowserContext, page: Page, mem: MemPage): Promise<Checkpoint<string>>;
17
+ export interface WorkerOpts {
18
+ traverseId: string;
19
+ workerIndex: number;
20
+ parallel: number;
21
+ maxSteps: number;
22
+ maxVisitsPerNode: number;
23
+ maxVisitsPerEdge: number;
24
+ timeoutMs: number;
25
+ from?: string;
26
+ startUrl?: string;
27
+ baseURL?: string;
28
+ data?: string;
29
+ graph: WaygraphGraph;
30
+ library: Awaited<ReturnType<typeof buildExploreContext>>["library"];
31
+ engine: Engine;
32
+ context: BrowserContext;
33
+ page: Page;
34
+ mem: MemPage;
35
+ leases: EdgeLeaseCoordinator;
36
+ }
37
+ export declare function runTraverseWorker(opts: WorkerOpts): Promise<TraverseWorkerResult>;
@@ -0,0 +1,200 @@
1
+ import { envString } from "../../engine/env.js";
2
+ import { buildFlow } from "../../engine/flow/flow.js";
3
+ import { ignorePageGone } from "../../engine/page-gone.js";
4
+ import { detectHere } from "../auto/auto.js";
5
+ import { buildExploreMenu } from "../auto/explore.js";
6
+ import { memStubValue } from "../auto/mem.js";
7
+ import { EdgeLeaseCoordinator } from "./lease.js";
8
+ export function seedMem(library, mem, dataJson) {
9
+ const raw = dataJson ?? envString("WAYGRAPH_DATA") ?? envString("WAYGRAPH_AUTO_MEM");
10
+ let envParsed = {};
11
+ if (raw) {
12
+ try {
13
+ envParsed = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ throw new Error(`waygraph traverse: --data / WAYGRAPH_DATA is not valid JSON`);
17
+ }
18
+ }
19
+ for (const entry of library.values()) {
20
+ for (const k of entry.block.requires ?? []) {
21
+ if (mem.has(k))
22
+ continue;
23
+ if (k.name in envParsed) {
24
+ mem.set(k, envParsed[k.name]);
25
+ continue;
26
+ }
27
+ const def = memStubValue(k);
28
+ if (def !== undefined)
29
+ mem.set(k, def);
30
+ }
31
+ }
32
+ }
33
+ export function snapshotMem(library, mem) {
34
+ const out = {};
35
+ for (const entry of library.values()) {
36
+ for (const k of entry.block.requires ?? []) {
37
+ if (mem.has(k) && !(k.name in out)) {
38
+ out[k.name] = mem.get(k);
39
+ }
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ export function restoreMem(library, mem, snap, dataJson) {
45
+ seedMem(library, mem, dataJson);
46
+ for (const entry of library.values()) {
47
+ for (const k of entry.block.requires ?? []) {
48
+ if (k.name in snap) {
49
+ mem.set(k, snap[k.name]);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ export function missingMemKeys(entry, mem) {
55
+ return (entry.block.requires ?? []).filter((k) => !mem.has(k));
56
+ }
57
+ export function ensureMem(entry, mem) {
58
+ const missing = missingMemKeys(entry, mem);
59
+ if (missing.length === 0)
60
+ return;
61
+ throw new Error(`Block "${entry.block.name}" needs Mem keys: ${missing.map((k) => k.name).join(", ")} ` +
62
+ `(pass --data '{...}' or WAYGRAPH_DATA)`);
63
+ }
64
+ export function edgeKey(edge) {
65
+ const id = edge.instanceOption?.id;
66
+ return id ? `${edge.block}::${id}` : edge.block;
67
+ }
68
+ export async function runOneBlock(engine, entry, context, page, mem) {
69
+ const flow = buildFlow([entry.block], engine.config);
70
+ const BLOCK_TIMEOUT_MS = 45000;
71
+ let timer;
72
+ try {
73
+ const ran = await Promise.race([
74
+ flow.run(context, mem, { page, closeOnFinish: false }),
75
+ new Promise((_, reject) => {
76
+ timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s`)), BLOCK_TIMEOUT_MS);
77
+ }),
78
+ ]);
79
+ return ran.result;
80
+ }
81
+ finally {
82
+ if (timer)
83
+ clearTimeout(timer);
84
+ }
85
+ }
86
+ export async function runTraverseWorker(opts) {
87
+ const { traverseId, workerIndex, parallel, maxSteps, maxVisitsPerNode, maxVisitsPerEdge, timeoutMs, graph, library, engine, context, mem, leases, } = opts;
88
+ let page = opts.page;
89
+ const startUrl = opts.startUrl;
90
+ const nodeVisits = new Map();
91
+ const edgeVisits = new Map();
92
+ const edgesHit = new Set();
93
+ let steps = 0;
94
+ const startedAt = Date.now();
95
+ let lastHere = opts.from ?? null;
96
+ let exitCode = 0;
97
+ let leaf;
98
+ try {
99
+ for (;;) {
100
+ if (Date.now() - startedAt > timeoutMs) {
101
+ console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
102
+ exitCode = 1;
103
+ break;
104
+ }
105
+ if (steps >= maxSteps) {
106
+ console.log(`[Killed max-steps[${traverseId}] node=${lastHere ?? "?"} steps=${steps} max=${maxSteps}]`);
107
+ exitCode = 1;
108
+ break;
109
+ }
110
+ if (page.isClosed()) {
111
+ page = await context.newPage();
112
+ if (startUrl) {
113
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(ignorePageGone);
114
+ }
115
+ }
116
+ const detected = await detectHere(page, library.navBlocks);
117
+ const here = lastHere ?? detected ?? opts.from ?? null;
118
+ if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
119
+ console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
120
+ exitCode = 1;
121
+ break;
122
+ }
123
+ const menu = await buildExploreMenu(page, graph, library, here);
124
+ const candidates = menu.flat.filter((edge) => {
125
+ const key = edgeKey(edge);
126
+ if ((edgeVisits.get(key) ?? 0) >= maxVisitsPerEdge)
127
+ return false;
128
+ if (parallel > 1) {
129
+ if (EdgeLeaseCoordinator.partitionIndex(key, parallel) !== workerIndex) {
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ });
135
+ if (candidates.length === 0) {
136
+ const label = here ?? detected ?? "unknown";
137
+ leaf = label;
138
+ console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
139
+ console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
140
+ break;
141
+ }
142
+ const pick = [...candidates].sort((a, b) => {
143
+ const aWild = a.from === "*" ? 1 : 0;
144
+ const bWild = b.from === "*" ? 1 : 0;
145
+ if (aWild !== bWild)
146
+ return aWild - bWild;
147
+ return edgeKey(a).localeCompare(edgeKey(b));
148
+ })[0];
149
+ const key = edgeKey(pick);
150
+ if (!leases.tryClaim(key, traverseId)) {
151
+ // Another worker won a race (instance fan-out); treat as unavailable.
152
+ edgeVisits.set(key, maxVisitsPerEdge);
153
+ continue;
154
+ }
155
+ const entry = library.byName.get(pick.block);
156
+ if (!entry) {
157
+ console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
158
+ exitCode = 1;
159
+ break;
160
+ }
161
+ const fromLabel = here ?? pick.from;
162
+ try {
163
+ if (pick.instanceOption) {
164
+ mem.set(pick.instanceOption.key, pick.instanceOption.value);
165
+ }
166
+ ensureMem(entry, mem);
167
+ console.error(`waygraph traverse[${traverseId}]: step ${steps + 1} ${fromLabel} -[${pick.block}]-> ${pick.to}` +
168
+ (pick.label ? ` (${pick.label})` : ""));
169
+ const out = await runOneBlock(engine, entry, context, page, mem);
170
+ const observed = typeof out === "object" && out && "__state" in out
171
+ ? String(out.__state)
172
+ : pick.to;
173
+ edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
174
+ edgesHit.add(key);
175
+ if (here)
176
+ nodeVisits.set(here, (nodeVisits.get(here) ?? 0) + 1);
177
+ lastHere = observed;
178
+ steps += 1;
179
+ if (observed !== pick.to && pick.to !== "*") {
180
+ console.error(`waygraph traverse[${traverseId}]: WARN static to=${pick.to} observed=${observed}`);
181
+ }
182
+ }
183
+ catch (err) {
184
+ const msg = err instanceof Error ? err.message : String(err);
185
+ console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${fromLabel} to=${pick.to}] ${msg}`);
186
+ exitCode = 1;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ finally {
192
+ console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
193
+ }
194
+ return {
195
+ exitCode,
196
+ steps,
197
+ edgesHit: [...edgesHit],
198
+ ...(leaf ? { leaf } : {}),
199
+ };
200
+ }
@@ -32,11 +32,11 @@ export async function findBlock(projectDir, ref) {
32
32
  if (byRuntimeName.length === 1)
33
33
  return byRuntimeName[0];
34
34
  if (byRuntimeName.length > 1) {
35
- throw new Error("waygraph chain: \"" + ref + "\" matches " + byRuntimeName.length + " Blocks by name - " +
35
+ throw new Error("waygraph run --blocks: \"" + ref + "\" matches " + byRuntimeName.length + " Blocks by name - " +
36
36
  byRuntimeName.map((b) => b.exportName + " (" + b.file + ")").join(", ") +
37
37
  ". Use the export name to disambiguate.");
38
38
  }
39
- throw new Error("waygraph chain: no Block named \"" + ref + "\" found under " + projectDir);
39
+ throw new Error("waygraph run --blocks: no Block named \"" + ref + "\" found under " + projectDir);
40
40
  }
41
41
  /**
42
42
  * Finds an already-defined Flow by its export name (e.g. loginFlow) among
@@ -24,7 +24,7 @@ async function findBareFlow(projectDir, spec) {
24
24
  function parseSegments(spec) {
25
25
  const segments = parseChainSpec(spec);
26
26
  if (segments.length === 0)
27
- throw new Error("waygraph chain: empty spec - give at least one block name");
27
+ throw new Error("waygraph run --blocks: empty spec - give at least one block name");
28
28
  return segments;
29
29
  }
30
30
  async function resolveSegment(projectDir, seg, mem, options) {
@@ -4,7 +4,7 @@ const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
4
4
  function seedMemFromRequires(mem, requires, json, label, stubActive) {
5
5
  if (requires.length === 0) {
6
6
  if (json !== undefined) {
7
- throw new Error("waygraph chain: \"" + label + "\" takes no input (empty requires) but got a payload: " + json);
7
+ throw new Error("waygraph run --blocks: \"" + label + "\" takes no input (empty requires) but got a payload: " + json);
8
8
  }
9
9
  return;
10
10
  }
@@ -22,7 +22,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
22
22
  haveParsed = true;
23
23
  }
24
24
  catch (err) {
25
- throw new Error("waygraph chain: \"" + label + "\" payload is not valid JSON - " + String(err));
25
+ throw new Error("waygraph run --blocks: \"" + label + "\" payload is not valid JSON - " + String(err));
26
26
  }
27
27
  }
28
28
  if (!haveParsed) {
@@ -30,7 +30,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
30
30
  // this flow; when it is, fall through with an empty object so the
31
31
  // per-key loop below gets a chance to fill each one from the registry.
32
32
  if (!stubActive) {
33
- throw new Error("waygraph chain: \"" + label + "\" requires " + requires.map((k) => k.name).join(", ") +
33
+ throw new Error("waygraph run --blocks: \"" + label + "\" requires " + requires.map((k) => k.name).join(", ") +
34
34
  " - give a JSON payload (inline blockName({...}) or --data '{...}'), or enable --mem-stub" +
35
35
  " with registerMemStub for these keys");
36
36
  }
@@ -54,7 +54,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
54
54
  return;
55
55
  }
56
56
  if (!isRecord(parsed)) {
57
- throw new Error("waygraph chain: \"" + label + "\" requires " + requires.length + " keys (" +
57
+ throw new Error("waygraph run --blocks: \"" + label + "\" requires " + requires.length + " keys (" +
58
58
  requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name");
59
59
  }
60
60
  const payload = parsed;
@@ -72,7 +72,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
72
72
  stillMissing.push(k);
73
73
  }
74
74
  if (stillMissing.length > 0) {
75
- throw new Error("waygraph chain: \"" + label + "\" payload is missing required key(s): " +
75
+ throw new Error("waygraph run --blocks: \"" + label + "\" payload is missing required key(s): " +
76
76
  stillMissing.map((k) => "\"" + k.name + "\"").join(", "));
77
77
  }
78
78
  }
@@ -8,7 +8,7 @@ export function parseChainSpec(spec) {
8
8
  .map((s) => {
9
9
  const m = /^([A-Za-z_$][\w-]*)\s*(?:\(([\s\S]*)\))?$/.exec(s);
10
10
  if (!m) {
11
- throw new Error("waygraph chain: could not parse segment \"" + s + "\" - expected \"blockName\" or \"blockName({...json...})\"");
11
+ throw new Error("waygraph run --blocks: could not parse segment \"" + s + "\" - expected \"blockName\" or \"blockName({...json...})\"");
12
12
  }
13
13
  const ref = m[1];
14
14
  const json = m[2] ? m[2].trim() : undefined;
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, Trait, narrate } from "waygraph";
1
+ import { defineMethodBlock, Trait } from "waygraph";
2
2
  import {
3
3
  CheckoutOverviewPage,
4
4
  OrderComplete,
@@ -21,9 +21,7 @@ export const FinishOrderBlock = defineMethodBlock({
21
21
  instruction: {
22
22
  async act(page) {
23
23
  const finishButton = page.locator(CheckoutOverviewSel.finishBtn);
24
- await narrate(finishButton, "Placing the order", () =>
25
- finishButton.click(),
26
- );
24
+ await finishButton.click();
27
25
  },
28
26
  verify: [
29
27
  Trait.text(
@@ -31,35 +29,5 @@ export const FinishOrderBlock = defineMethodBlock({
31
29
  "Thank you for your order!",
32
30
  ),
33
31
  ],
34
- // Shown AFTER act (order already placed) - selectors must match the
35
- // completion page, not #finish on the overview.
36
- slides: [
37
- {
38
- caption: "Checkout is a short pipeline",
39
- detail: "Cart -> info -> overview -> done. You just placed the order.",
40
- tag: "YAP",
41
- tone: "info",
42
- duration: true,
43
- },
44
- {
45
- caption: "Completion page is the durable proof",
46
- detail:
47
- "tone: success - the thank-you header is what verify asserts on.",
48
- tag: "YAP",
49
- tone: "success",
50
- selector: CheckoutOverviewSel.completeHeader,
51
- duration: 2500,
52
- fastMode: 700,
53
- },
54
- {
55
- caption: "Episode continues after this yap",
56
- detail: "Next step rings the same banner, then you move on.",
57
- tag: "YAP",
58
- tone: "planned",
59
- selector: CheckoutOverviewSel.completeHeader,
60
- duration: true,
61
- fastMode: 500,
62
- },
63
- ],
64
32
  },
65
33
  });
@@ -12,6 +12,7 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
12
12
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
13
13
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
14
14
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
15
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
15
16
  import { NavBackToProductsBlock } from "../blocks/saucedemo-web/inventory-item/nav-back-to-products.block.js";
16
17
 
17
18
  /** Full checkout, then the "Back Home" click-based Nav Block returns to the catalogue. */
@@ -35,6 +36,6 @@ export const checkoutThenBrowseAgainFlow = engine
35
36
  .method(FillLastNameBlock)
36
37
  .method(FillPostalCodeBlock)
37
38
  .method(SubmitCheckoutInfoBlock)
38
- .method(FinishOrderBlock)
39
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
39
40
  .gotoPage(NavBackToProductsBlock)
40
41
  .end();
@@ -12,6 +12,7 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
12
12
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
13
13
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
14
14
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
15
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
15
16
  import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
16
17
 
17
18
  export const checkoutFlow = engine
@@ -80,5 +81,5 @@ export const checkoutFlow = engine
80
81
  }),
81
82
  )
82
83
  .method(SubmitCheckoutInfoBlock.stubBefore((ctx) => ctx.hideCaption()))
83
- .method(FinishOrderBlock)
84
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
84
85
  .end();
@@ -8,6 +8,7 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
8
8
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
9
9
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
10
10
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
11
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
11
12
 
12
13
  /** Add the selected item, check out and place the order. Starts signed in (run it after loginFlow). */
13
14
  export const shopFlow = engine
@@ -20,5 +21,5 @@ export const shopFlow = engine
20
21
  .method(FillLastNameBlock)
21
22
  .method(FillPostalCodeBlock)
22
23
  .method(SubmitCheckoutInfoBlock)
23
- .method(FinishOrderBlock)
24
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
24
25
  .end();