@emseepea/testing 0.23.2 → 0.23.4

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 CHANGED
@@ -109,12 +109,30 @@ EMSEEPEA_CODEX_MODEL=gpt-5.5 emseepea-test --provider codex-local eval
109
109
 
110
110
  `codex-local` uses the existing Codex CLI sign-in without copying it.
111
111
  `codex-ci` requires `OPENAI_API_KEY` from the CI secret store. Both providers
112
- run `codex exec --json`, resume the actual Codex session for follow-ups, ignore
113
- user configuration, and pre-approve only the discovered tools from the test
114
- server.
112
+ support Codex CLI 0.162.0 or later, run `codex exec --json`, resume the actual
113
+ Codex session for follow-ups, ignore user configuration, and pre-approve only
114
+ the discovered tools from the test server.
115
115
 
116
116
  An isolated command policy denies shell launches and spawned shell environments
117
- inherit no provider credentials.
117
+ inherit no provider credentials. The runner also disables Codex account apps,
118
+ plugins, remote plugins, multi-agent execution, web search, shell tools, and
119
+ unified execution for each one-shot and resumed native invocation.
120
+
121
+ The invocation configuration sets the default account-app state to disabled,
122
+ omits account-app instructions, and disables Codex app, plugin, and tool
123
+ suggestion features.
124
+
125
+ Native-client events are authoritative for the tools that were actually used:
126
+ a turn is accepted only when every application MCP call belongs to the
127
+ configured test server and its expected tools.
128
+
129
+ Codex may make its two built-in resource-discovery calls through the CLI's
130
+ `codex` server. The runner accepts them only when their native results prove the
131
+ resource and template inventories are empty, and excludes them from application
132
+ assertions and raw evidence.
133
+
134
+ Any non-empty, unrecognised, account-app, plugin, or other foreign call is
135
+ rejected without storing raw account metadata.
118
136
 
119
137
  Failed Codex runs retain only a fixed failure category: authentication,
120
138
  configuration, host storage, permission or sandbox, provider, transport, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/testing",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "MCP integration and semantic testing helpers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -6,6 +6,11 @@ import { scriptedElicitations } from "./elicitation.mjs";
6
6
 
7
7
  const claudeModel = "claude-sonnet-4-6";
8
8
  const mcpServerName = "emseepea_eval";
9
+ const minimumCodexCliVersion = "0.162.0";
10
+ const codexEmptyDiscoveryTools = new Map([
11
+ ["list_mcp_resources", "resources"],
12
+ ["list_mcp_resource_templates", "resourceTemplates"],
13
+ ]);
9
14
  const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
10
15
  const providerErrorMessages = new Map([
11
16
  ["error_during_execution", "Model command failed during execution"],
@@ -39,6 +44,9 @@ export async function modelVersion(provider = "claude-local") {
39
44
  const result = await runProcess(command, ["--version"], { env: modelEnvironment({}) });
40
45
  const version = result.stdout.trim().match(/(?:codex-cli\s+)?(\d+\.\d+\.\d+)\b/)?.[1];
41
46
  if (result.code !== 0 || !version) throw new Error(`Could not verify ${codex ? "Codex" : "Claude"} CLI version`);
47
+ if (codex && compareVersions(version, minimumCodexCliVersion) < 0) {
48
+ throw new Error(`Codex CLI ${version} is unsupported; use ${minimumCodexCliVersion} or later`);
49
+ }
42
50
  return version;
43
51
  }
44
52
 
@@ -53,11 +61,16 @@ export function providerSettings(provider) {
53
61
  return provider.startsWith("codex-")
54
62
  ? {
55
63
  approvalPolicy: "never",
64
+ appInstructions: false,
65
+ apps: false,
56
66
  loginShell: false,
57
67
  multiAgent: false,
68
+ plugins: false,
69
+ remotePlugin: false,
58
70
  sandbox: "read-only",
59
71
  shell: "forbidden",
60
72
  shellEnvironment: "empty",
73
+ toolSuggest: false,
61
74
  userConfig: "ignored",
62
75
  webSearch: false,
63
76
  }
@@ -453,7 +466,13 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
453
466
  const forbidden = completed.filter((item) => !["agent_message", "reasoning", "mcp_tool_call"].includes(item?.type));
454
467
  if (forbidden.length) throw forbiddenItemError(forbidden);
455
468
  const toolItems = completed.filter(({ type }) => type === "mcp_tool_call");
456
- const calls = toolItems.map((item) => {
469
+ const discoveryItems = toolItems.filter(({ server, tool }) =>
470
+ server === "codex" && codexEmptyDiscoveryTools.has(tool));
471
+ if (discoveryItems.some((item) => !emptyCodexDiscovery(item))) {
472
+ throw forbiddenItemError(discoveryItems.filter((item) => !emptyCodexDiscovery(item)));
473
+ }
474
+ const applicationToolItems = toolItems.filter((item) => !discoveryItems.includes(item));
475
+ const calls = applicationToolItems.map((item) => {
457
476
  if (item.server !== mcpServerName || !advertised.has(item.tool)
458
477
  || !item.arguments || typeof item.arguments !== "object" || Array.isArray(item.arguments)) {
459
478
  throw forbiddenItemError([item]);
@@ -467,7 +486,7 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
467
486
  const messages = completed.filter(({ type }) => type === "agent_message");
468
487
  const answer = messages.at(-1)?.text;
469
488
  if (typeof answer !== "string") throw new Error("Model command returned no answer");
470
- const toolResults = toolItems.map(({ result }) => ({ content: result.content ?? result, isError: false }));
489
+ const toolResults = applicationToolItems.map(({ result }) => ({ content: result.content ?? result, isError: false }));
471
490
  return {
472
491
  answer,
473
492
  calls,
@@ -487,6 +506,25 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
487
506
  };
488
507
  }
489
508
 
509
+ function emptyCodexDiscovery(item) {
510
+ if (item.status !== "completed" || !item.arguments || typeof item.arguments !== "object"
511
+ || Array.isArray(item.arguments) || Object.keys(item.arguments).length !== 0
512
+ || !item.result || typeof item.result !== "object" || Array.isArray(item.result)
513
+ || item.result.structured_content !== null || Object.keys(item.result).length !== 2
514
+ || !Array.isArray(item.result.content) || item.result.content.length !== 1) return false;
515
+ const [content] = item.result.content;
516
+ if (!content || content.type !== "text" || typeof content.text !== "string"
517
+ || Object.keys(content).length !== 2) return false;
518
+ try {
519
+ const parsed = JSON.parse(content.text);
520
+ const key = codexEmptyDiscoveryTools.get(item.tool);
521
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
522
+ && Object.keys(parsed).length === 1 && Array.isArray(parsed[key]) && parsed[key].length === 0;
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
527
+
490
528
  function startCodexConversation(provider, directory, url, tools, authToken, context, signal, serverSecrets) {
491
529
  let threadId;
492
530
  let closed = false;
@@ -545,8 +583,14 @@ function codexInvocation(provider, prompt, directory, conversation) {
545
583
  "-c", 'web_search="disabled"',
546
584
  "-c", 'shell_environment_policy.inherit="none"',
547
585
  "-c", "allow_login_shell=false",
586
+ "-c", "apps._default.enabled=false",
587
+ "-c", "include_apps_instructions=false",
588
+ "-c", "features.apps=false",
548
589
  "-c", "features.multi_agent=false",
590
+ "-c", "features.plugins=false",
591
+ "-c", "features.remote_plugin=false",
549
592
  "-c", "features.shell_tool=false",
593
+ "-c", "features.tool_suggest=false",
550
594
  "-c", "features.unified_exec=false",
551
595
  ];
552
596
  if (conversation?.context) args.push("-c", `developer_instructions=${JSON.stringify(conversation.context)}`);
@@ -633,6 +677,16 @@ function classifyCodexFailure(stderr) {
633
677
  return "unknown";
634
678
  }
635
679
 
680
+ function compareVersions(left, right) {
681
+ const a = left.split(".").map(Number);
682
+ const b = right.split(".").map(Number);
683
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
684
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
685
+ if (difference !== 0) return difference;
686
+ }
687
+ return 0;
688
+ }
689
+
636
690
  function codexEvidenceError(message, category) {
637
691
  return Object.assign(new Error(message), {
638
692
  providerFailureCategory: codexFailureCategories.has(category) ? category : "unknown",