@molecule/api-ai-tools 1.9.19 → 1.10.1
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.
- package/README.md +104 -7
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -1
- package/dist/suite-overlap.d.ts +26 -1
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +66 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +237 -19
- package/dist/trace-artifacts.d.ts +38 -0
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +212 -2
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts +9 -4
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +6 -4
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { posix } from 'path';
|
|
10
|
-
import { commandCutRun, CUT_AT_BUDGET, cutRunSentence, suiteRunExitCode } from './cut-run.js';
|
|
10
|
+
import { commandCutRun, CUT_AT_BUDGET, cutRunSentence, suiteRunExitCode, suiteSummary, } from './cut-run.js';
|
|
11
11
|
import { addChangeSpan, changedSpan, countLines, renderEditExcerpt, renderSmallFile, } from './edit-excerpt.js';
|
|
12
12
|
import { guardToolExecute, missingParamError, ToolInputError } from './input-normalizer.js';
|
|
13
13
|
import { diffLineCount } from './line-diff.js';
|
|
@@ -171,6 +171,105 @@ export async function taskStopCommand(backend, taskId) {
|
|
|
171
171
|
return fallback;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* How often a wait re-asks whether a task whose exit file has not appeared has ended. The exit
|
|
176
|
+
* file is read every tick; this probe is a process test plus the task's own output, and 3 s is
|
|
177
|
+
* close enough that a run which ended is never waited out (m182: 281 s on a walk that ended in 20).
|
|
178
|
+
*/
|
|
179
|
+
const TASK_END_LOOK_MS = 3_000;
|
|
180
|
+
/** How much of a task's own output the end test reads — a runner's tally is its last lines. */
|
|
181
|
+
const TASK_OUTPUT_END_BYTES = 4_000;
|
|
182
|
+
/**
|
|
183
|
+
* The shell probe behind {@link taskEndState}: the pid the launcher wrote, and the end of the
|
|
184
|
+
* file the command redirects its own output to.
|
|
185
|
+
*
|
|
186
|
+
* The pid test is the one `molecule-dev/api/src/ai/background-tasks.ts` applies in `liveness` —
|
|
187
|
+
* a written exit code ends the task, a live pid keeps it running, and a handle the sandbox cannot
|
|
188
|
+
* account for stays unknown — so this package and that one cannot disagree about whether a task
|
|
189
|
+
* is still walking.
|
|
190
|
+
*
|
|
191
|
+
* @param taskId - The task's `mol-bg-…` handle (format already checked by the caller).
|
|
192
|
+
* @param own - The file the command redirects its own output to, when the launch could tell.
|
|
193
|
+
* @returns The command; its first line is `MOL_TASK_STATE <alive|gone|unknown>`, the rest the tail.
|
|
194
|
+
*/
|
|
195
|
+
const taskStateCommand = (taskId, own) => `P=$(cat /tmp/${taskId}.pid 2>/dev/null); ` +
|
|
196
|
+
`if [ -z "$P" ] || [ ! -f /tmp/${taskId}.sh ]; then echo MOL_TASK_STATE unknown; ` +
|
|
197
|
+
`elif kill -0 "$P" 2>/dev/null; then echo MOL_TASK_STATE alive; ` +
|
|
198
|
+
`else echo MOL_TASK_STATE gone; fi; ` +
|
|
199
|
+
(own ? `tail -c ${TASK_OUTPUT_END_BYTES} ${shellQuote(own)} 2>/dev/null` : 'true');
|
|
200
|
+
/**
|
|
201
|
+
* The ending a task's own output already records, read from the file its command redirects to.
|
|
202
|
+
*
|
|
203
|
+
* A command can put its real work in a subshell and leave the platform's wrapper waiting on
|
|
204
|
+
* something else — `( runner > /tmp/full-run.log; echo EXIT=$? >> /tmp/full-run.log ) &` — so
|
|
205
|
+
* the task log stays empty and the exit file may never appear while the run itself is over. The
|
|
206
|
+
* run's own file is where its outcome is: an `EXIT=<n>` line the command appended, or the
|
|
207
|
+
* runner's closing tally (Playwright's status lines, Vitest's `Tests …` line, Hurl's
|
|
208
|
+
* `Executed files:`), which is the last thing a finished run prints.
|
|
209
|
+
*
|
|
210
|
+
* @param text - The tail of the task's own output file.
|
|
211
|
+
* @returns Whether the output records an ending, and the code when it recorded one.
|
|
212
|
+
*/
|
|
213
|
+
function outputEnding(text) {
|
|
214
|
+
const trimmed = text.trimEnd();
|
|
215
|
+
if (!trimmed)
|
|
216
|
+
return { ended: false, exitCode: null };
|
|
217
|
+
// An explicit marker the command appended is the run's own verdict, whatever precedes it.
|
|
218
|
+
const marker = /(?:^|\n)EXIT=(\d+)\s*$/.exec(trimmed);
|
|
219
|
+
if (marker)
|
|
220
|
+
return { ended: true, exitCode: Number(marker[1]) };
|
|
221
|
+
const lastLine = trimmed.slice(trimmed.lastIndexOf('\n') + 1).trim();
|
|
222
|
+
// A tally is an ending only as the LAST line: a second walk starts by printing its own header,
|
|
223
|
+
// so a file whose last line is still the first walk's tally is between the two.
|
|
224
|
+
const tallyLine = /^\d+\s+(?:passed|failed|flaky|skipped|interrupted|timed out|did not run)\b/.test(lastLine) ||
|
|
225
|
+
/^Tests\s+.+\(\d+\)$/.test(lastLine) ||
|
|
226
|
+
/\bExecuted files:\s*\d+/.test(lastLine);
|
|
227
|
+
if (!tallyLine)
|
|
228
|
+
return { ended: false, exitCode: null };
|
|
229
|
+
return { ended: suiteSummary(trimmed)?.finished ?? false, exitCode: null };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether a background task has ENDED, asked of the task itself rather than of a note about it.
|
|
233
|
+
*
|
|
234
|
+
* A missing exit file alone is not "running": the launcher writes that file from the wrapper it
|
|
235
|
+
* started, and a command that backgrounds its real work leaves the file missing after the run is
|
|
236
|
+
* over. X1 xr-molecule-flash-m182 (+30:25) waited 281 s — its whole remaining ceiling — on a walk
|
|
237
|
+
* that had ended in 20 s, and a `sleep 240; tail` on the same handle was answered "is live". The
|
|
238
|
+
* end is read the way `molecule-dev/api/src/ai/background-tasks.ts` reads it — exit file, then
|
|
239
|
+
* the pid — plus the one signal this package has and that tracker does not: the task's own output
|
|
240
|
+
* file already holding its ending.
|
|
241
|
+
*
|
|
242
|
+
* @param backend - The execution backend the task runs on.
|
|
243
|
+
* @param taskId - The task's `mol-bg-…` handle.
|
|
244
|
+
* @returns Whether it has ended, and the code its own output recorded when it recorded one.
|
|
245
|
+
*/
|
|
246
|
+
async function taskEndState(backend, taskId) {
|
|
247
|
+
const own = outputFileByTask.get(taskId);
|
|
248
|
+
let stdout;
|
|
249
|
+
try {
|
|
250
|
+
const r = await backend.run(taskStateCommand(taskId, own), { timeout: 5_000 });
|
|
251
|
+
stdout = String(r.stdout ?? '');
|
|
252
|
+
}
|
|
253
|
+
catch (_error) {
|
|
254
|
+
// A sandbox that cannot answer is not evidence of an ending: the quiet failure (a wait) beats
|
|
255
|
+
// the loud one (a result handed back for a run that is still walking).
|
|
256
|
+
return { ended: false, exitCode: null };
|
|
257
|
+
}
|
|
258
|
+
const [stateLine = '', ...rest] = stdout.split('\n');
|
|
259
|
+
const state = /MOL_TASK_STATE (alive|gone|unknown)/.exec(stateLine)?.[1] ?? 'unknown';
|
|
260
|
+
const ownEnding = outputEnding(rest.join('\n'));
|
|
261
|
+
// The run's own output can end the task while its wrapper is still alive: the wrapper may be
|
|
262
|
+
// waiting on the wrong thing (m182's was blocked) while the run it started is over. An explicit
|
|
263
|
+
// `EXIT=<n>` is that verdict; a bare tally is trusted only once the pid is gone, because the
|
|
264
|
+
// instant between a walk's tally and its next walk's header reads the same.
|
|
265
|
+
if (ownEnding.ended && (ownEnding.exitCode !== null || state === 'gone'))
|
|
266
|
+
return ownEnding;
|
|
267
|
+
// A pid this sandbox can account for and that is gone, with no exit file: the wrapper that
|
|
268
|
+
// writes the exit code was killed or exited, so the task ended and has no verdict to invent.
|
|
269
|
+
if (state === 'gone')
|
|
270
|
+
return { ended: true, exitCode: null };
|
|
271
|
+
return { ended: false, exitCode: null };
|
|
272
|
+
}
|
|
174
273
|
/** A millisecond span the way a model reads it: seconds under 90, else minutes. */
|
|
175
274
|
function durationText(ms) {
|
|
176
275
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
@@ -382,17 +481,27 @@ async function backgroundTaskState(backend, command, seconds, ceilingMs) {
|
|
|
382
481
|
const own = outputFileByTask.get(id);
|
|
383
482
|
return own !== undefined && command.includes(own);
|
|
384
483
|
};
|
|
484
|
+
// A task is only a candidate to WAIT on while it is still walking: the exit file and the same
|
|
485
|
+
// end test `wait_for_task` applies both say when it ended, so a sleep is never held for a handle
|
|
486
|
+
// whose run is already over (m182 +30:25: `sleep 240; tail /tmp/full-run.log` was answered "is
|
|
487
|
+
// live" for a task that had ended 20 s after it started).
|
|
488
|
+
const stateOf = async (id) => {
|
|
489
|
+
const exitCode = await exitCodeOf(id);
|
|
490
|
+
if (exitCode !== null)
|
|
491
|
+
return { id, ended: true, exitCode };
|
|
492
|
+
return { id, ...(await taskEndState(backend, id)) };
|
|
493
|
+
};
|
|
385
494
|
const newestFirst = [...ids].reverse();
|
|
386
495
|
const namedId = newestFirst.find(names);
|
|
387
496
|
let target = namedId
|
|
388
|
-
?
|
|
497
|
+
? await stateOf(namedId)
|
|
389
498
|
: null;
|
|
390
499
|
if (!target) {
|
|
391
500
|
// Bounded: a long session can leave many finished tasks behind, and each look costs a read.
|
|
392
501
|
for (const id of newestFirst.slice(0, 8)) {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
target =
|
|
502
|
+
const state = await stateOf(id);
|
|
503
|
+
if (!state.ended) {
|
|
504
|
+
target = state;
|
|
396
505
|
break;
|
|
397
506
|
}
|
|
398
507
|
}
|
|
@@ -408,7 +517,7 @@ async function backgroundTaskState(backend, command, seconds, ceilingMs) {
|
|
|
408
517
|
// No slice: the note still says the task's state and how to get its result.
|
|
409
518
|
}
|
|
410
519
|
const secondsText = `Did not sleep ${Math.round(seconds)} s: background task \`${target.id}\``;
|
|
411
|
-
if (target.
|
|
520
|
+
if (!target.ended) {
|
|
412
521
|
const facts = waitProgressClauses({
|
|
413
522
|
taskId: target.id,
|
|
414
523
|
slice,
|
|
@@ -427,8 +536,12 @@ async function backgroundTaskState(backend, command, seconds, ceilingMs) {
|
|
|
427
536
|
const counter = logProgress(slice);
|
|
428
537
|
const progress = counter ? `, at ${counter.done} of ${counter.total} in its log` : '';
|
|
429
538
|
const failures = slice.trim() ? '; the failures in its log are in outputSoFar' : '';
|
|
539
|
+
// No exit file and no code in the run's own output: it ended and there is no verdict to invent.
|
|
540
|
+
const verdict = target.exitCode === null
|
|
541
|
+
? 'no exit code — its process is gone and its output recorded none'
|
|
542
|
+
: `exit code ${target.exitCode}`;
|
|
430
543
|
return {
|
|
431
|
-
note: `${secondsText} had already ended (
|
|
544
|
+
note: `${secondsText} had already ended (${verdict})${progress}${failures} — ` +
|
|
432
545
|
`\`wait_for_task ${target.id}\` returns its result now.`,
|
|
433
546
|
slice,
|
|
434
547
|
};
|
|
@@ -483,6 +596,62 @@ export const SUITE_OVER_BUDGET_ADVICE = 'A test suite is not a bigger unit to sp
|
|
|
483
596
|
* sentence, which names an option the executor cannot set.
|
|
484
597
|
*/
|
|
485
598
|
const SEARCH_TIMEOUT_MS = 10_000;
|
|
599
|
+
/**
|
|
600
|
+
* The most alternatives one brace group in a `search_files` include may expand
|
|
601
|
+
* to. The point is to accept the ordinary multi-extension form a model writes
|
|
602
|
+
* from habit (`*.{ts,tsx,yml,json,md}`), not to grow a glob engine: a group is
|
|
603
|
+
* bounded so a pattern cannot turn one grep call into hundreds.
|
|
604
|
+
*/
|
|
605
|
+
const MAX_INCLUDE_BRACE_ALTERNATIVES = 16;
|
|
606
|
+
/**
|
|
607
|
+
* Expand ONE level of brace alternatives in a `search_files` include pattern,
|
|
608
|
+
* validating every expanded pattern.
|
|
609
|
+
*
|
|
610
|
+
* `grep --include` has no brace expansion of its own (the shell that would do it
|
|
611
|
+
* is never involved — the pattern is `shellQuote`d first), so the group has to
|
|
612
|
+
* be expanded here and each alternative passed as its own `--include`. The
|
|
613
|
+
* m189 run wrote the everyday `*.{ts,tsx,yml,json,md}` and the search was
|
|
614
|
+
* refused outright, costing a round trip and pushing the agent to a narrower
|
|
615
|
+
* search.
|
|
616
|
+
*
|
|
617
|
+
* Bounded on purpose: one level of braces, alternatives split on `,`, at most
|
|
618
|
+
* {@link MAX_INCLUDE_BRACE_ALTERNATIVES}, none empty, and each EXPANDED
|
|
619
|
+
* pattern must still pass {@link isValidGlob} — so a shell metacharacter in any
|
|
620
|
+
* alternative (`*.{ts,$(rm)}`) is refused exactly as a bare one is.
|
|
621
|
+
*
|
|
622
|
+
* @param pattern - The user-supplied `include` value.
|
|
623
|
+
* @returns Every pattern to pass to grep, or null when the value is not
|
|
624
|
+
* accepted (the caller reports the include-glob error).
|
|
625
|
+
*/
|
|
626
|
+
function expandIncludeGlob(pattern) {
|
|
627
|
+
const open = pattern.indexOf('{');
|
|
628
|
+
if (open === -1)
|
|
629
|
+
return isValidGlob(pattern) ? [pattern] : null;
|
|
630
|
+
const close = pattern.indexOf('}', open + 1);
|
|
631
|
+
if (close === -1)
|
|
632
|
+
return null;
|
|
633
|
+
// One level only: a second `{` before the close or a second `}` after it is a
|
|
634
|
+
// nested/malformed group, not an alternative.
|
|
635
|
+
const nextOpen = pattern.indexOf('{', open + 1);
|
|
636
|
+
if (nextOpen !== -1 && nextOpen < close)
|
|
637
|
+
return null;
|
|
638
|
+
if (pattern.indexOf('}', close + 1) !== -1)
|
|
639
|
+
return null;
|
|
640
|
+
const alternatives = pattern.slice(open + 1, close).split(',');
|
|
641
|
+
if (alternatives.length > MAX_INCLUDE_BRACE_ALTERNATIVES)
|
|
642
|
+
return null;
|
|
643
|
+
if (alternatives.some((alt) => alt.length === 0))
|
|
644
|
+
return null;
|
|
645
|
+
const expanded = alternatives.map((alt) => `${pattern.slice(0, open)}${alt}${pattern.slice(close + 1)}`);
|
|
646
|
+
return expanded.every((p) => isValidGlob(p)) ? expanded : null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* The include-glob error. It keeps the original character-rule sentence and
|
|
650
|
+
* names the accepted multi-extension form, so a refusal teaches the syntax
|
|
651
|
+
* rather than only reporting that the value was wrong.
|
|
652
|
+
*/
|
|
653
|
+
const INCLUDE_GLOB_ERROR = 'Invalid include glob pattern. Allowed: alphanumeric and @ * ? . _ - / [ ] ( ), ' +
|
|
654
|
+
'or one brace group of up to 16 comma-separated alternatives such as *.{ts,tsx}.';
|
|
486
655
|
/**
|
|
487
656
|
* The share of its budget a command must use before the "you asked for more than
|
|
488
657
|
* this tool's ceiling" note is worth attaching. Below it the clamp changed nothing
|
|
@@ -728,7 +897,7 @@ function packageCatalog(path) {
|
|
|
728
897
|
* @returns Array of AITool objects ready to pass to an AI provider
|
|
729
898
|
*/
|
|
730
899
|
export function buildTools(backend, config) {
|
|
731
|
-
const { include, exclude, pathGuards = true, symlinkGuards = false, redactSecrets: doRedact = true, blockDangerousCommands = false, blockCommand, execTimeoutMs = 120_000, commandBudgetMs, overCeilingGraceMs = OVER_CEILING_GRACE_MS, searchExcludedDirs, outlineLargeHtml, excludedTestTitles, onAfterWrite, onFileDiff, onFileChange, } = config ?? {};
|
|
900
|
+
const { include, exclude, pathGuards = true, symlinkGuards = false, redactSecrets: doRedact = true, blockDangerousCommands = false, blockCommand, execTimeoutMs = 120_000, commandBudgetMs, overCeilingGraceMs = OVER_CEILING_GRACE_MS, searchExcludedDirs, outlineLargeHtml, excludedTestTitles, requiredSuiteEnv, onAfterWrite, onFileDiff, onFileChange, } = config ?? {};
|
|
732
901
|
const root = backend.projectRoot;
|
|
733
902
|
// A host-filesystem backend with escape guards off is the combination the
|
|
734
903
|
// audit flagged: `pathGuards` off lets `..` paths out of the workspace,
|
|
@@ -1541,7 +1710,14 @@ export function buildTools(backend, config) {
|
|
|
1541
1710
|
await backend.writeFile(path, content);
|
|
1542
1711
|
if (onAfterWrite)
|
|
1543
1712
|
await onAfterWrite(path).catch(() => { });
|
|
1544
|
-
|
|
1713
|
+
// The text on disk is the text just written unless an after-write hook (a formatter)
|
|
1714
|
+
// may have rewritten it. Without one, the re-read is a whole sandbox round trip per
|
|
1715
|
+
// write that can only return what is already in hand — measured in the write path's
|
|
1716
|
+
// own timings (x0 fast-file-writes: four sandbox calls per write, one of them this
|
|
1717
|
+
// re-read, and the excerpt it feeds is identical either way).
|
|
1718
|
+
const finalContent = onAfterWrite
|
|
1719
|
+
? await backend.readFile(path).catch(() => content)
|
|
1720
|
+
: content;
|
|
1545
1721
|
const diff = computeDiff(oldContent, finalContent);
|
|
1546
1722
|
if (onFileChange)
|
|
1547
1723
|
onFileChange({ type: oldContent === null ? 'created' : 'modified', path });
|
|
@@ -2066,15 +2242,18 @@ export function buildTools(backend, config) {
|
|
|
2066
2242
|
const contextLines = Number.isFinite(rawContext)
|
|
2067
2243
|
? Math.min(Math.max(Math.trunc(rawContext), 0), MAX_SEARCH_CONTEXT_LINES)
|
|
2068
2244
|
: 0;
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2245
|
+
// `--include` has no brace expansion inside the quoted grep argument, so
|
|
2246
|
+
// a brace group is expanded here and grep gets one --include per pattern.
|
|
2247
|
+
const includes = include ? expandIncludeGlob(include) : [];
|
|
2248
|
+
if (includes === null)
|
|
2249
|
+
return { error: INCLUDE_GLOB_ERROR };
|
|
2073
2250
|
const symlinkErr = await checkSymlink(path);
|
|
2074
2251
|
if (symlinkErr)
|
|
2075
2252
|
return { error: symlinkErr };
|
|
2076
2253
|
try {
|
|
2077
|
-
const globArg =
|
|
2254
|
+
const globArg = includes.length
|
|
2255
|
+
? includes.map((p) => `--include=${shellQuote(p)}`).join(' ')
|
|
2256
|
+
: '';
|
|
2078
2257
|
// With context, grep emits `file-line-content` for context lines and
|
|
2079
2258
|
// `file:line:content` for matches, plus `--` group separators. Both
|
|
2080
2259
|
// shapes are parsed below; without context the output is unchanged.
|
|
@@ -2900,6 +3079,8 @@ export function buildTools(backend, config) {
|
|
|
2900
3079
|
const started = runByTask.get(taskId);
|
|
2901
3080
|
let midRunNote = null;
|
|
2902
3081
|
let lastCauseLookAt = startedAt - repeatingCauseLookMs();
|
|
3082
|
+
let lastEndLookAt = startedAt - TASK_END_LOOK_MS;
|
|
3083
|
+
let endedWithoutCode = false;
|
|
2903
3084
|
let exitCode = await readExit();
|
|
2904
3085
|
while (exitCode === null && Date.now() - startedAt < budgetMs) {
|
|
2905
3086
|
await new Promise((r) => setTimeout(r, Math.min(2_000, budgetMs - (Date.now() - startedAt))));
|
|
@@ -2908,6 +3089,19 @@ export function buildTools(backend, config) {
|
|
|
2908
3089
|
// its real result instead of a note about it.
|
|
2909
3090
|
if (exitCode !== null)
|
|
2910
3091
|
break;
|
|
3092
|
+
// A missing exit file is not "running": the launcher writes that file from its wrapper,
|
|
3093
|
+
// and a command that backgrounds its real work leaves it missing after the run is over.
|
|
3094
|
+
// The same end test the sleep peek uses answers here, before a cause look can `continue`
|
|
3095
|
+
// past it — m182 (+30:25) waited 281 s on a walk that had ended in 20 s.
|
|
3096
|
+
if (Date.now() - lastEndLookAt >= TASK_END_LOOK_MS) {
|
|
3097
|
+
lastEndLookAt = Date.now();
|
|
3098
|
+
const state = await taskEndState(backend, taskId);
|
|
3099
|
+
if (state.ended) {
|
|
3100
|
+
exitCode = state.exitCode;
|
|
3101
|
+
endedWithoutCode = state.exitCode === null;
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
2911
3105
|
if (repeatingCauseNoted.has(taskId))
|
|
2912
3106
|
continue;
|
|
2913
3107
|
if (Date.now() - lastCauseLookAt < repeatingCauseLookMs())
|
|
@@ -2987,7 +3181,10 @@ export function buildTools(backend, config) {
|
|
|
2987
3181
|
}
|
|
2988
3182
|
const waitedMs = Date.now() - startedAt;
|
|
2989
3183
|
waitedByTask.set(taskId, waitedBefore + waitedMs);
|
|
2990
|
-
|
|
3184
|
+
// A task the end test found over with no exit code is FINISHED, not still walking: its
|
|
3185
|
+
// result is read below from its own output. Only a task still alive (or a sandbox that
|
|
3186
|
+
// could not answer) reaches the running/stuck answers.
|
|
3187
|
+
if (exitCode === null && !endedWithoutCode) {
|
|
2991
3188
|
const grownTo = await logBytes();
|
|
2992
3189
|
const lastSize = logBytesByTask.get(taskId);
|
|
2993
3190
|
if (grownTo !== null)
|
|
@@ -3138,12 +3335,20 @@ export function buildTools(backend, config) {
|
|
|
3138
3335
|
// failing tests instead of leaving the executor to re-run the whole suite (xr-molecule-
|
|
3139
3336
|
// flash-m101 +12:13: 139 tests, ~4 minutes, to check two fixes).
|
|
3140
3337
|
const run = runByTask.get(taskId);
|
|
3338
|
+
// What the REQUEST requires of this suite and the run left out: the narrowed re-run below
|
|
3339
|
+
// is composed from the run's own command, so without these it would measure the same wrong
|
|
3340
|
+
// configuration again (m182: the request names `TEST_MODE=fullstack`, the run and its
|
|
3341
|
+
// re-run both omitted it, and the walk measured the `spa` suite). The consumer's reading,
|
|
3342
|
+
// asked once per finished run; a failure to read answers "nothing required".
|
|
3343
|
+
const requiredEnv = run && requiredSuiteEnv
|
|
3344
|
+
? ((await requiredSuiteEnv(run.command, run.cwd).catch(() => [])) ?? [])
|
|
3345
|
+
: [];
|
|
3141
3346
|
// The tests the request excludes are not causes to fix, and the re-run below inverts them
|
|
3142
3347
|
// out — a result must never ask the executor to fix a test it also says does not count.
|
|
3143
3348
|
// The log's own path rides along so a recap that leaves causes out can hand over the ONE
|
|
3144
3349
|
// grep that prints their bodies: the stdout above is cut in the middle, so the log is where
|
|
3145
3350
|
// they still are whole.
|
|
3146
|
-
const causes = failureCausesNote(output, excludedTestTitles?.() ?? [], run ? { ...run, log: ownFile ?? log } : undefined);
|
|
3351
|
+
const causes = failureCausesNote(output, excludedTestTitles?.() ?? [], run ? { ...run, log: ownFile ?? log, env: requiredEnv } : undefined);
|
|
3147
3352
|
// A backgrounded run is under no tool budget, and the exit file hands back the PIPELINE's
|
|
3148
3353
|
// code — so a suite killed at a `timeout 280` the command carried behind a `| tail` reports
|
|
3149
3354
|
// 0 here too. The run's start is in its own id ({@link taskStartedAt}), so the bound it used
|
|
@@ -3177,7 +3382,16 @@ export function buildTools(backend, config) {
|
|
|
3177
3382
|
? suiteRunPipelinePlacement(run.command)
|
|
3178
3383
|
: { piped: false, unplaced: false };
|
|
3179
3384
|
const reported = suiteRunExitCode(exitCode, output, placement.piped);
|
|
3180
|
-
|
|
3385
|
+
// A run that ended with no code at all is not a filtered pipeline: the sentence below says
|
|
3386
|
+
// where its result came from, and claiming a stage after the runner would contradict it.
|
|
3387
|
+
const noCodeNote = endedWithoutCode ? null : suiteExitCodeNote(placement, reported);
|
|
3388
|
+
// The task ended and no exit file recorded a verdict: say so, and say where the result came
|
|
3389
|
+
// from, so the executor reads it instead of waiting on a handle that can never answer.
|
|
3390
|
+
const endedNote = endedWithoutCode
|
|
3391
|
+
? 'It ended without writing an exit code — its process is gone' +
|
|
3392
|
+
(ownFile ? `, and its own output (${ownFile}) is the result below` : '') +
|
|
3393
|
+
'. Read that result; do not wait on this task again.'
|
|
3394
|
+
: null;
|
|
3181
3395
|
// A FINISHED result says so, even when the code it would report is withheld: a filtered
|
|
3182
3396
|
// pipeline's code is not the run's verdict ({@link FILTERED_EXIT_NOTE}), so `exitCode` alone
|
|
3183
3397
|
// cannot tell a caller this is the delivered result rather than a progress return — and a
|
|
@@ -3187,8 +3401,12 @@ export function buildTools(backend, config) {
|
|
|
3187
3401
|
status: 'completed',
|
|
3188
3402
|
exitCode: reported,
|
|
3189
3403
|
waitedMs,
|
|
3190
|
-
...(causes || noCodeNote
|
|
3191
|
-
? {
|
|
3404
|
+
...(causes || noCodeNote || endedNote
|
|
3405
|
+
? {
|
|
3406
|
+
note: [causes, noCodeNote, endedNote]
|
|
3407
|
+
.filter((n) => Boolean(n))
|
|
3408
|
+
.join(' '),
|
|
3409
|
+
}
|
|
3192
3410
|
: {}),
|
|
3193
3411
|
stdout: sanitizeOutput(truncateMiddle(output, MAX_OUTPUT_SIZE)),
|
|
3194
3412
|
...(output.length > MAX_OUTPUT_SIZE ? { truncated: true, fullOutput: log } : {}),
|
|
@@ -190,6 +190,29 @@ export interface TraceMissingText {
|
|
|
190
190
|
/** The path of the response that carried it (`/api/tags`). */
|
|
191
191
|
path: string;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* A write the trace recorded answering a boolean field, and a LATER GET of the same resource
|
|
195
|
+
* answering the opposite for that field.
|
|
196
|
+
*
|
|
197
|
+
* This is the pair that reads as a frontend race and is not one: xr-molecule-flash-m181's follow
|
|
198
|
+
* test sent `POST /api/profiles/:username/follow` and the API answered
|
|
199
|
+
* `{"profile":{…,"following":true}}`, then a `GET /api/profiles/:username` answered
|
|
200
|
+
* `{"profile":{…,"following":false}}` — the READ route never saw the viewer, so it hydrated
|
|
201
|
+
* `following` from nobody. The run rewrote the profile component twice, the API client once, and
|
|
202
|
+
* added optimistic state before anyone read the route's middleware; the write itself was fine.
|
|
203
|
+
*/
|
|
204
|
+
export interface TraceReadContradiction {
|
|
205
|
+
/** The boolean field both answered (`following`, `favorited`). */
|
|
206
|
+
field: string;
|
|
207
|
+
/** What the write's response said it is. */
|
|
208
|
+
writeValue: boolean;
|
|
209
|
+
/** What the later read's response said it is. */
|
|
210
|
+
readValue: boolean;
|
|
211
|
+
/** The write that answered it (`POST /api/profiles/x/follow`). */
|
|
212
|
+
write: TraceRequest;
|
|
213
|
+
/** The later GET of the same resource (`GET /api/profiles/x`). */
|
|
214
|
+
read: TraceRequest;
|
|
215
|
+
}
|
|
193
216
|
/** What one trace says: the test it belongs to, the requests it made, and its console lines. */
|
|
194
217
|
export interface TraceArtifact {
|
|
195
218
|
/** The failure's spec file, relative to the run's directory, when the trace names one. */
|
|
@@ -266,6 +289,15 @@ export interface TraceArtifact {
|
|
|
266
289
|
* read before this field existed.
|
|
267
290
|
*/
|
|
268
291
|
missingText?: TraceMissingText;
|
|
292
|
+
/**
|
|
293
|
+
* A write's answer a later GET of the same resource contradicted — the read route does not see
|
|
294
|
+
* the viewer (xr-molecule-flash-m181's `following`). The note leads with it and sends the run at
|
|
295
|
+
* the route's middleware, not the page's state handling.
|
|
296
|
+
*
|
|
297
|
+
* Absent when no write and later read in the trace answered the same boolean field differently,
|
|
298
|
+
* and on traces read before this field existed.
|
|
299
|
+
*/
|
|
300
|
+
readContradiction?: TraceReadContradiction;
|
|
269
301
|
}
|
|
270
302
|
/** How many traces one read reports, newest first. */
|
|
271
303
|
export declare const MAX_TRACE_ARTIFACTS = 12;
|
|
@@ -491,6 +523,12 @@ export declare function isCredentialExchangePath(path: string, authEndpoints?: r
|
|
|
491
523
|
* this on the deployed URL only, where the background session verification lands later than it
|
|
492
524
|
* does locally.
|
|
493
525
|
*
|
|
526
|
+
* A write whose response said a boolean field is true and a LATER GET of the same resource that
|
|
527
|
+
* said it is false leads the note first ({@link readContradictionSentence}): the read route never
|
|
528
|
+
* saw the viewer, so the fix is on that route's middleware, not in the page. xr-molecule-flash-m181
|
|
529
|
+
* spent ~8 minutes rewriting the profile component over exactly that pair — the write answered
|
|
530
|
+
* `{"following":true}` and the next profile GET answered `false`.
|
|
531
|
+
*
|
|
494
532
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
495
533
|
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
496
534
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
|