@emseepea/testing 0.23.1 → 0.23.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.
- package/README.md +9 -0
- package/package.json +1 -1
- package/semantic/provider.mjs +102 -13
- package/semantic/test.mjs +24 -4
package/README.md
CHANGED
|
@@ -116,6 +116,15 @@ server.
|
|
|
116
116
|
An isolated command policy denies shell launches and spawned shell environments
|
|
117
117
|
inherit no provider credentials.
|
|
118
118
|
|
|
119
|
+
Failed Codex runs retain only a fixed failure category: authentication,
|
|
120
|
+
configuration, host storage, permission or sandbox, provider, transport, or
|
|
121
|
+
unknown. The runner inspects at most 32 KiB of standard error to derive that
|
|
122
|
+
category and does not store the standard error.
|
|
123
|
+
|
|
124
|
+
Rejected native items retain only capped counts from a fixed item-type list.
|
|
125
|
+
Commands, arguments, results, paths, server and tool names, and unknown
|
|
126
|
+
item-type strings are excluded.
|
|
127
|
+
|
|
119
128
|
Codex CLI evidence proves that CLI journey. It does not prove native ChatGPT
|
|
120
129
|
connection, attachment, retrieval, or MCP App rendering behavior.
|
|
121
130
|
|
package/package.json
CHANGED
package/semantic/provider.mjs
CHANGED
|
@@ -13,6 +13,17 @@ const providerErrorMessages = new Map([
|
|
|
13
13
|
["error_max_turns", "Model command exceeded its turn limit"],
|
|
14
14
|
]);
|
|
15
15
|
const authenticationFailure = /not logged in|failed to authenticate|oauth session expired/i;
|
|
16
|
+
const codexFailureCategories = new Set([
|
|
17
|
+
"authentication",
|
|
18
|
+
"configuration",
|
|
19
|
+
"host-storage",
|
|
20
|
+
"permission-or-sandbox",
|
|
21
|
+
"provider",
|
|
22
|
+
"transport",
|
|
23
|
+
"unknown",
|
|
24
|
+
]);
|
|
25
|
+
const stderrLimit = 32 * 1024;
|
|
26
|
+
const forbiddenItemLimit = 64;
|
|
16
27
|
const hasAuthenticationFailure = (events) => events.some((event) => (
|
|
17
28
|
(Array.isArray(event.message?.content)
|
|
18
29
|
&& event.message.content.some(({ type, text }) => type === "text" && /not logged in/i.test(text ?? "")))
|
|
@@ -388,14 +399,17 @@ export async function runModel(provider, prompt, directory, signal) {
|
|
|
388
399
|
if (execution.outputLimitExceeded) throw new Error("Model command output exceeded its limit");
|
|
389
400
|
if (execution.errorCode === "ABORT_ERR") throw new Error("Model command was cancelled");
|
|
390
401
|
if (execution.errorCode) throw new Error("Model command could not start");
|
|
391
|
-
if (execution.code !== 0 && !execution.stdout)
|
|
402
|
+
if (execution.code !== 0 && !execution.stdout) {
|
|
403
|
+
throw codexProcessError(execution.code, execution.providerFailureCategory);
|
|
404
|
+
}
|
|
392
405
|
return provider.startsWith("codex-")
|
|
393
406
|
? parseCodexEvents(execution.stdout, [], undefined, execution.code,
|
|
394
|
-
providerModel(provider), invocation.providerSecrets)
|
|
407
|
+
providerModel(provider), invocation.providerSecrets, execution.providerFailureCategory)
|
|
395
408
|
: parseClaudeEvents(execution.stdout, execution.code);
|
|
396
409
|
}
|
|
397
410
|
|
|
398
|
-
export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, processExitCode = 0,
|
|
411
|
+
export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, processExitCode = 0,
|
|
412
|
+
selectedModel = "test-model", secrets = [], providerFailureCategory) {
|
|
399
413
|
let events;
|
|
400
414
|
try {
|
|
401
415
|
events = typeof stdout === "string"
|
|
@@ -412,11 +426,13 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
412
426
|
const authentication = /(?:not logged in|authentication|unauthorized|invalid api key|oauth)/i;
|
|
413
427
|
const errorText = events.flatMap((event) => [event.message, event.error?.message, event.item?.message])
|
|
414
428
|
.filter((value) => typeof value === "string").join("\n");
|
|
415
|
-
if (authentication.test(errorText))
|
|
429
|
+
if (authentication.test(errorText)) {
|
|
430
|
+
throw codexEvidenceError("Model command is not signed in", "authentication");
|
|
431
|
+
}
|
|
416
432
|
const failedTool = events.some(({ item }) => item?.type === "mcp_tool_call"
|
|
417
433
|
&& (item.status === "failed" || item.result?.isError === true || item.result?.is_error === true));
|
|
418
434
|
if (failedTool) throw new Error("Model command reported a failed MCP tool call");
|
|
419
|
-
if (processExitCode !== 0) throw
|
|
435
|
+
if (processExitCode !== 0) throw codexProcessError(processExitCode, providerFailureCategory);
|
|
420
436
|
if (events.some(({ type }) => type === "turn.failed" || type === "error")) {
|
|
421
437
|
throw new Error("Model command reported an error");
|
|
422
438
|
}
|
|
@@ -435,12 +451,12 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
435
451
|
const advertised = new Set(advertisedTools.map(({ name }) => name));
|
|
436
452
|
const completed = events.filter(({ type }) => type === "item.completed").map(({ item }) => item);
|
|
437
453
|
const forbidden = completed.filter((item) => !["agent_message", "reasoning", "mcp_tool_call"].includes(item?.type));
|
|
438
|
-
if (forbidden.length) throw
|
|
454
|
+
if (forbidden.length) throw forbiddenItemError(forbidden);
|
|
439
455
|
const toolItems = completed.filter(({ type }) => type === "mcp_tool_call");
|
|
440
456
|
const calls = toolItems.map((item) => {
|
|
441
457
|
if (item.server !== mcpServerName || !advertised.has(item.tool)
|
|
442
458
|
|| !item.arguments || typeof item.arguments !== "object" || Array.isArray(item.arguments)) {
|
|
443
|
-
throw
|
|
459
|
+
throw forbiddenItemError([item]);
|
|
444
460
|
}
|
|
445
461
|
if (!item.result || typeof item.result !== "object" || item.status === "failed"
|
|
446
462
|
|| item.result.isError === true || item.result.is_error === true) {
|
|
@@ -492,7 +508,8 @@ function startCodexConversation(provider, directory, url, tools, authToken, cont
|
|
|
492
508
|
if (execution.errorCode === "ABORT_ERR") throw new Error("Model command was cancelled");
|
|
493
509
|
if (execution.errorCode) throw new Error("Model conversation could not start");
|
|
494
510
|
const turn = parseCodexEvents(execution.stdout, tools, threadId, execution.code,
|
|
495
|
-
providerModel(provider), [...invocation.providerSecrets, authToken, ...serverSecrets]
|
|
511
|
+
providerModel(provider), [...invocation.providerSecrets, authToken, ...serverSecrets],
|
|
512
|
+
execution.providerFailureCategory);
|
|
496
513
|
threadId = turn.threadId;
|
|
497
514
|
return { ...turn, elicitations: [] };
|
|
498
515
|
},
|
|
@@ -559,8 +576,10 @@ function runProcess(command, args, options) {
|
|
|
559
576
|
return new Promise((resolve) => {
|
|
560
577
|
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], ...options });
|
|
561
578
|
let stdout = "";
|
|
579
|
+
let stderr = Buffer.alloc(0);
|
|
562
580
|
let timedOut = false;
|
|
563
581
|
let outputLimitExceeded = false;
|
|
582
|
+
let settled = false;
|
|
564
583
|
const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 180_000);
|
|
565
584
|
timer.unref();
|
|
566
585
|
child.stdout.on("data", (chunk) => {
|
|
@@ -570,18 +589,88 @@ function runProcess(command, args, options) {
|
|
|
570
589
|
child.kill("SIGKILL");
|
|
571
590
|
}
|
|
572
591
|
});
|
|
573
|
-
child.stderr.
|
|
574
|
-
|
|
592
|
+
child.stderr.on("data", (chunk) => {
|
|
593
|
+
if (stderr.length >= stderrLimit) return;
|
|
594
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
595
|
+
stderr = Buffer.concat([stderr, bytes.subarray(0, stderrLimit - stderr.length)]);
|
|
596
|
+
});
|
|
597
|
+
const finish = (result) => {
|
|
598
|
+
if (settled) return;
|
|
599
|
+
settled = true;
|
|
575
600
|
clearTimeout(timer);
|
|
576
|
-
|
|
601
|
+
const providerFailureCategory = classifyCodexFailure(stderr.toString("utf8"));
|
|
602
|
+
stderr = Buffer.alloc(0);
|
|
603
|
+
resolve({ ...result, providerFailureCategory });
|
|
604
|
+
};
|
|
605
|
+
child.once("error", (error) => {
|
|
606
|
+
finish({ code: 1, errorCode: error.code, outputLimitExceeded, stdout, timedOut });
|
|
577
607
|
});
|
|
578
608
|
child.once("close", (code) => {
|
|
579
|
-
|
|
580
|
-
resolve({ code: code ?? 1, outputLimitExceeded, stdout, timedOut });
|
|
609
|
+
finish({ code: code ?? 1, outputLimitExceeded, stdout, timedOut });
|
|
581
610
|
});
|
|
582
611
|
});
|
|
583
612
|
}
|
|
584
613
|
|
|
614
|
+
function classifyCodexFailure(stderr) {
|
|
615
|
+
if (/no space left|disk (?:is )?full|storage (?:capacity|quota)|\benospc\b/i.test(stderr)) {
|
|
616
|
+
return "host-storage";
|
|
617
|
+
}
|
|
618
|
+
if (/not logged in|authenticat|unauthorized|invalid api key|oauth/i.test(stderr)) {
|
|
619
|
+
return "authentication";
|
|
620
|
+
}
|
|
621
|
+
if (/permission denied|operation not permitted|sandbox|\beacces\b|\beperm\b/i.test(stderr)) {
|
|
622
|
+
return "permission-or-sandbox";
|
|
623
|
+
}
|
|
624
|
+
if (/configuration|config(?:uration)? error|unknown (?:feature|field|key)|invalid (?:config|value)/i.test(stderr)) {
|
|
625
|
+
return "configuration";
|
|
626
|
+
}
|
|
627
|
+
if (/connection (?:refused|reset)|network|transport|timed? out|\btls\b|certificate|\bdns\b|\beconn/i.test(stderr)) {
|
|
628
|
+
return "transport";
|
|
629
|
+
}
|
|
630
|
+
if (/provider (?:error|failure|unavailable)|model (?:is )?(?:unavailable|overloaded)|rate limit|service unavailable/i.test(stderr)) {
|
|
631
|
+
return "provider";
|
|
632
|
+
}
|
|
633
|
+
return "unknown";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function codexEvidenceError(message, category) {
|
|
637
|
+
return Object.assign(new Error(message), {
|
|
638
|
+
providerFailureCategory: codexFailureCategories.has(category) ? category : "unknown",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function codexProcessError(code, category) {
|
|
643
|
+
return codexEvidenceError(`Model command exited ${code}`, category);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function forbiddenItemError(items) {
|
|
647
|
+
const counts = {
|
|
648
|
+
commandExecution: 0,
|
|
649
|
+
computerUse: 0,
|
|
650
|
+
fileChange: 0,
|
|
651
|
+
mcpToolCall: 0,
|
|
652
|
+
other: 0,
|
|
653
|
+
webSearch: 0,
|
|
654
|
+
};
|
|
655
|
+
for (const item of items.slice(0, forbiddenItemLimit)) {
|
|
656
|
+
const key = new Map([
|
|
657
|
+
["command_execution", "commandExecution"],
|
|
658
|
+
["computer_tool", "computerUse"],
|
|
659
|
+
["file_change", "fileChange"],
|
|
660
|
+
["mcp_tool_call", "mcpToolCall"],
|
|
661
|
+
["web_search", "webSearch"],
|
|
662
|
+
]).get(item?.type) ?? "other";
|
|
663
|
+
counts[key] += 1;
|
|
664
|
+
}
|
|
665
|
+
return Object.assign(new Error("Model command used a forbidden tool"), {
|
|
666
|
+
forbiddenItemEvidence: Object.freeze({
|
|
667
|
+
counts: Object.freeze(counts),
|
|
668
|
+
inspectedCount: Math.min(items.length, forbiddenItemLimit),
|
|
669
|
+
truncated: items.length > forbiddenItemLimit,
|
|
670
|
+
}),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
585
674
|
function modelEnvironment(extra) {
|
|
586
675
|
return Object.fromEntries(Object.entries({
|
|
587
676
|
CI: "true",
|
package/semantic/test.mjs
CHANGED
|
@@ -178,7 +178,7 @@ export async function createConversation(testContext, options) {
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
const trial = state.trials[index];
|
|
181
|
-
trial.record
|
|
181
|
+
recordModelFailure(trial.record, result.reason);
|
|
182
182
|
if (Array.isArray(result.reason?.elicitations)) trial.record.elicitations = result.reason.elicitations;
|
|
183
183
|
if (Array.isArray(result.reason?.attemptedToolCalls)) {
|
|
184
184
|
trial.record.attemptedToolCalls = result.reason.attemptedToolCalls;
|
|
@@ -508,9 +508,11 @@ async function judgeTrialMeaning(trial, trialIndex, expected) {
|
|
|
508
508
|
record.verdict = verdict;
|
|
509
509
|
outcomes.push({ failed: !verdict.pass, record });
|
|
510
510
|
} catch (error) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
if (error instanceof SyntaxError || error.message === "Judge returned an invalid verdict") {
|
|
512
|
+
record.error = "invalid judge verdict";
|
|
513
|
+
} else {
|
|
514
|
+
recordModelFailure(record, error);
|
|
515
|
+
}
|
|
514
516
|
outcomes.push({ failed: true, record });
|
|
515
517
|
}
|
|
516
518
|
}
|
|
@@ -570,6 +572,24 @@ function safeModelFailure(error) {
|
|
|
570
572
|
return "model invocation failed";
|
|
571
573
|
}
|
|
572
574
|
|
|
575
|
+
function recordModelFailure(record, error) {
|
|
576
|
+
record.error = safeModelFailure(error);
|
|
577
|
+
const category = error?.providerFailureCategory;
|
|
578
|
+
if ([
|
|
579
|
+
"authentication",
|
|
580
|
+
"configuration",
|
|
581
|
+
"host-storage",
|
|
582
|
+
"permission-or-sandbox",
|
|
583
|
+
"provider",
|
|
584
|
+
"transport",
|
|
585
|
+
"unknown",
|
|
586
|
+
].includes(category)) record.providerFailureCategory = category;
|
|
587
|
+
const evidence = error?.forbiddenItemEvidence;
|
|
588
|
+
if (evidence && typeof evidence === "object") {
|
|
589
|
+
record.forbiddenItemEvidence = structuredClone(evidence);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
573
593
|
async function isolatedModel(provider, prompt, prefix, signal) {
|
|
574
594
|
const directory = await mkdtemp(join(tmpdir(), prefix));
|
|
575
595
|
try {
|