ravensight-playtest 0.7.0 → 0.7.5

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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which failed actions are the game's state and which are the runner's.
3
+ *
4
+ * A web page that shows a control as disabled, hidden or not editable has
5
+ * refused the action itself: the click failing is what a player would see.
6
+ * In a real Lab Ledger run the "Run repeat panel" button stayed disabled
7
+ * after the page said it was ready, and the blocker the persona filed was
8
+ * dropped as "driver errors only" because Playwright's click timed out on
9
+ * a button it had found and that said `disabled`. That is evidence.
10
+ *
11
+ * A failure where the runner itself broke stays a driver error: a target
12
+ * that never resolved (a selector or ref problem), a browser or page that
13
+ * closed or crashed under the runner.
14
+ */
15
+
16
+ /** Playwright's actionability waits that name the game's state. */
17
+ const NOT_ACTIONABLE = /element is not (enabled|editable|visible)/i;
18
+
19
+ /** The element a locator resolved to, from Playwright's call log. */
20
+ const RESOLVED = /locator resolved to (<[^\n]*)/i;
21
+
22
+ /** A resolved element whose own markup says it is disabled. */
23
+ const RESOLVED_DISABLED = /locator resolved to <[^>]*\s(?:disabled(?:[\s=>/]|$)|aria-disabled="true")/i;
24
+
25
+ /** The runner's browser, context or page went away under it. */
26
+ const RUNNER_GONE = /Target (?:page, context or browser has been closed|crashed)|browser has (?:been )?disconnected|Browser closed|Page crashed/i;
27
+
28
+ const STATE_WORDS = { enabled: 'disabled', editable: 'not editable', visible: 'hidden' };
29
+
30
+ /**
31
+ * The game's refusal named in a failed action's error, or null when the
32
+ * failure is not one.
33
+ *
34
+ * @param {string} text the driver's error text (a call log may be cut short)
35
+ * @returns {{state: string, element: string|null}|null}
36
+ */
37
+ export function gameRefusal(text) {
38
+ const value = String(text || '');
39
+ if (!value || RUNNER_GONE.test(value)) return null;
40
+ const resolved = value.match(RESOLVED);
41
+ if (!resolved) return null;
42
+ const element = resolved[1].trim().slice(0, 160);
43
+ const waited = value.match(NOT_ACTIONABLE);
44
+ if (waited) return { state: STATE_WORDS[waited[1].toLowerCase()], element };
45
+ if (RESOLVED_DISABLED.test(value)) return { state: 'disabled', element };
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * True when the error says the runner's browser or page went away.
51
+ *
52
+ * @param {string} text
53
+ * @returns {boolean}
54
+ */
55
+ export function runnerGone(text) {
56
+ return RUNNER_GONE.test(String(text || ''));
57
+ }
package/src/run/model.js CHANGED
@@ -170,7 +170,24 @@ export function toProxyError(error) {
170
170
  if (key !== 'type' && key !== 'message') figures[key] = value;
171
171
  }
172
172
  }
173
- return new ProxyError({ status, code, message, retryAfter, figures });
173
+ return new ProxyError({ status, code, message: withProviderReason(message, figures), retryAfter, figures });
174
+ }
175
+
176
+ /**
177
+ * The provider's own reason, when the proxy passed one on (a 400
178
+ * `upstream_rejected` carries `provider_type` and a cleaned
179
+ * `provider_message`), added to the message the developer is shown. Without
180
+ * it a refused body reads only "The model provider rejected this request."
181
+ *
182
+ * @param {string} message
183
+ * @param {Object} figures
184
+ * @returns {string}
185
+ */
186
+ export function withProviderReason(message, figures = {}) {
187
+ const type = typeof figures.provider_type === 'string' ? figures.provider_type : '';
188
+ const reason = typeof figures.provider_message === 'string' ? figures.provider_message : '';
189
+ if (!type && !reason) return message;
190
+ return `${message} Provider said: ${[type, reason].filter(Boolean).join(': ')}`;
174
191
  }
175
192
 
176
193
  /**
package/src/run/openPr.js CHANGED
@@ -85,7 +85,14 @@ export async function openPullRequest({ repoRoot, branch, paths, title: rawTitle
85
85
  ...paths.map(file => ` ${plainTitle(file)}`)
86
86
  ].join('\n');
87
87
  // `yes: false` on purpose: --yes agreed to the price, not to publishing.
88
- const go = await confirm(question, { yes: false });
88
+ let go;
89
+ try { go = await confirm(question, { yes: false }); }
90
+ catch (error) {
91
+ // --yes cannot agree to publishing, so with no terminal the answer is a
92
+ // plain refusal, never a piped "y".
93
+ if (error?.code === 'no_terminal') return refuse('--open-pr asks before it pushes, and there is no terminal to ask in.');
94
+ throw error;
95
+ }
89
96
  if (!go) return refuse('Not confirmed.');
90
97
 
91
98
  const added = await call('git', ['add', '--', ...paths]);