@elench/testkit 0.1.94 → 0.1.95

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.
@@ -22,17 +22,14 @@ export function startCodexHostedSession({
22
22
  } = {}) {
23
23
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-codex-"));
24
24
  const outputFile = path.join(tempDir, "final-message.txt");
25
- const args = ["exec", "--json", "-o", outputFile];
26
-
27
- if (purpose === "assistant") {
28
- args.push("-s", "read-only");
29
- }
30
- if (model) {
31
- args.push("--model", String(model));
32
- }
33
- args.push(...normalizeProviderArgs(providerArgs));
34
-
35
- args.push(prompt);
25
+ const args = buildCodexArgs({
26
+ outputFile,
27
+ purpose,
28
+ model,
29
+ providerArgs,
30
+ prompt,
31
+ sandbox: process.env.TESTKIT_CODEX_SANDBOX,
32
+ });
36
33
 
37
34
  const child = execa(command, args, {
38
35
  cwd,
@@ -65,6 +62,28 @@ export function startCodexHostedSession({
65
62
  };
66
63
  }
67
64
 
65
+ export function buildCodexArgs({
66
+ outputFile,
67
+ purpose = "assistant",
68
+ model = null,
69
+ providerArgs = [],
70
+ prompt = "",
71
+ sandbox = null,
72
+ } = {}) {
73
+ const args = ["exec", "--json"];
74
+ if (outputFile) args.push("-o", outputFile);
75
+
76
+ if (purpose === "assistant") {
77
+ args.push("-s", String(sandbox || "workspace-write"));
78
+ }
79
+ if (model) {
80
+ args.push("--model", String(model));
81
+ }
82
+ args.push(...normalizeProviderArgs(providerArgs));
83
+ args.push(prompt);
84
+ return args;
85
+ }
86
+
68
87
  function normalizeProviderArgs(providerArgs) {
69
88
  if (!Array.isArray(providerArgs)) return [];
70
89
  return providerArgs.flatMap((arg) => String(arg || "").split(/\s+/).filter(Boolean));
@@ -160,7 +160,8 @@ function buildContextMarkdown(productDir, snapshot, paths) {
160
160
  lines.push(
161
161
  "",
162
162
  "## Guidance",
163
- "- Use shell commands like `npm run testkit`, `npx testkit`, or `testkit run --dir . --type <type>` when you need to execute tests.",
163
+ "- Use shell commands like `npm run testkit`, `npx testkit`, or `testkit run <type> --dir .` when you need to execute tests.",
164
+ "- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
164
165
  "- Use the command log and focused context files before rereading artifacts manually.",
165
166
  "- Prefer repo-local commands over guessing project-specific wrappers.",
166
167
  ""
@@ -173,15 +174,15 @@ function buildCommandsMarkdown() {
173
174
  return [
174
175
  "# Testkit Commands",
175
176
  "",
176
- "- `testkit run --dir . --type int`",
177
- "- `testkit run --dir . --type e2e`",
177
+ "- `testkit run int --dir .`",
178
+ "- `testkit run e2e --dir .`",
178
179
  "- `testkit run --dir . --file path/to/file.testkit.ts`",
179
180
  "- `testkit discover --dir .`",
180
181
  "- `testkit status --dir .`",
181
182
  "- `testkit doctor --dir .`",
182
183
  "- `testkit destroy --dir .`",
183
184
  "- `npm run testkit`",
184
- "- `npx testkit --dir . --type e2e`",
185
+ "- `npx testkit run e2e --dir .`",
185
186
  "",
186
187
  ].join("\n");
187
188
  }
@@ -177,11 +177,19 @@ export function createAssistantState({
177
177
  setProvider(nextProvider) {
178
178
  settings = mergeAssistantSettings(settings, { provider: nextProvider || DEFAULT_ASSISTANT_SETTINGS.provider });
179
179
  resolvedProviderName = null;
180
+ if (settings.model && getModelProviderMismatch(resolveInitialProvider(settings.provider, env), settings.model)) {
181
+ settings = mergeAssistantSettings(settings, { model: null });
182
+ }
180
183
  saveAssistantSettings(productDir, settings);
181
184
  notify();
182
185
  },
183
186
 
184
187
  setModel(nextModel) {
188
+ const resolvedProvider = resolveInitialProvider(settings.provider, env);
189
+ const mismatch = getModelProviderMismatch(resolvedProvider, nextModel);
190
+ if (mismatch) {
191
+ throw new Error(mismatch);
192
+ }
185
193
  settings = mergeAssistantSettings(settings, { model: nextModel || null });
186
194
  saveAssistantSettings(productDir, settings);
187
195
  notify();
@@ -345,6 +353,22 @@ function resolveInitialProvider(provider, env) {
345
353
  return null;
346
354
  }
347
355
 
356
+ function getModelProviderMismatch(provider, model) {
357
+ const normalizedModel = String(model || "").trim().toLowerCase();
358
+ if (!provider || !normalizedModel) return null;
359
+
360
+ const looksClaude = /\b(?:opus|sonnet|haiku|claude)\b/.test(normalizedModel);
361
+ const looksCodex = /\b(?:gpt|codex|o[1-9]|chatgpt)\b/.test(normalizedModel);
362
+
363
+ if (provider === "codex" && looksClaude) {
364
+ return `Model "${model}" looks like a Claude model, but the assistant is using Codex. Run /provider claude or /model default.`;
365
+ }
366
+ if (provider === "claude" && looksCodex) {
367
+ return `Model "${model}" looks like a Codex/OpenAI model, but the assistant is using Claude. Run /provider codex or /model default.`;
368
+ }
369
+ return null;
370
+ }
371
+
348
372
  async function executeSlashCommand({
349
373
  slash,
350
374
  state,
@@ -496,9 +520,16 @@ async function executeSlashTool(slash, context) {
496
520
  }
497
521
 
498
522
  function buildRunSlashCommand(options = {}) {
499
- const parts = ["testkit", "run", "--dir", "."];
500
- for (const type of options.type || []) {
501
- parts.push("--type", type);
523
+ const types = options.type || [];
524
+ const parts = ["testkit", "run"];
525
+ if (types.length === 1) {
526
+ parts.push(types[0]);
527
+ }
528
+ parts.push("--dir", ".");
529
+ if (types.length !== 1) {
530
+ for (const type of types) {
531
+ parts.push("--type", type);
532
+ }
502
533
  }
503
534
  for (const suite of options.suite || []) {
504
535
  parts.push("--suite", suite);
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { execa } from "execa";
4
- import { buildExecutionEnv } from "../runner/template.mjs";
4
+ import { buildTemplateExecutionEnv } from "../runner/template.mjs";
5
5
  import {
6
6
  collectConfiguredInputs,
7
7
  runConfiguredSteps,
@@ -13,7 +13,7 @@ export async function runTemplateStage(config, stageName, databaseUrl, options =
13
13
  if (steps.length === 0) return;
14
14
 
15
15
  const env = {
16
- ...buildExecutionEnv(config, {}, process.env),
16
+ ...buildTemplateExecutionEnv(config, {}, process.env),
17
17
  DATABASE_URL: databaseUrl,
18
18
  };
19
19
 
@@ -54,7 +54,7 @@ export async function captureTemplateSnapshot(config, outputPath, databaseUrl, o
54
54
  {
55
55
  cwd: config.productDir,
56
56
  env: {
57
- ...buildExecutionEnv(config, {}, process.env),
57
+ ...buildTemplateExecutionEnv(config, {}, process.env),
58
58
  DATABASE_URL: templateDbUrl,
59
59
  },
60
60
  stdout: "pipe",
@@ -127,6 +127,7 @@ export async function computeRuntimePrepareFingerprint(config) {
127
127
  : null,
128
128
  })
129
129
  );
130
+ hash.update(JSON.stringify(collectRuntimeDatabaseFingerprintInputs(config)));
130
131
 
131
132
  for (const envFile of config.testkit.envFiles || []) {
132
133
  appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile));
@@ -138,6 +139,41 @@ export async function computeRuntimePrepareFingerprint(config) {
138
139
  return hash.digest("hex");
139
140
  }
140
141
 
142
+ function collectRuntimeDatabaseFingerprintInputs(config) {
143
+ const inputs = [];
144
+ const ownDatabaseUrl = readDatabaseUrl(config.stateDir);
145
+ if (ownDatabaseUrl) {
146
+ inputs.push({ service: config.name, url: ownDatabaseUrl });
147
+ }
148
+
149
+ const referencedServices = new Set();
150
+ for (const value of Object.values(config.testkit.serviceEnv || {})) {
151
+ collectDatabasePlaceholderServices(value, referencedServices, config.name);
152
+ }
153
+ for (const value of Object.values(config.testkit.local?.env || {})) {
154
+ collectDatabasePlaceholderServices(value, referencedServices, config.name);
155
+ }
156
+
157
+ const stateDirByService = config.testkit.templateContext?.stateDirByService;
158
+ for (const serviceName of [...referencedServices].sort()) {
159
+ const stateDir = stateDirByService?.get?.(serviceName);
160
+ const databaseUrl = stateDir ? readDatabaseUrl(stateDir) : null;
161
+ inputs.push({ service: serviceName, url: databaseUrl || null });
162
+ }
163
+
164
+ return inputs;
165
+ }
166
+
167
+ function collectDatabasePlaceholderServices(value, out, defaultServiceName) {
168
+ if (typeof value !== "string") return;
169
+ const matcher = /\{db(?:Url|Host|Port|Name|User|Password)(?::([a-zA-Z0-9_-]+))?\}/g;
170
+ let match = matcher.exec(value);
171
+ while (match) {
172
+ out.add(match[1] || defaultServiceName);
173
+ match = matcher.exec(value);
174
+ }
175
+ }
176
+
141
177
  function appendResolvedInputToHash(hash, productDir, absPath) {
142
178
  const relative = path.relative(productDir, absPath);
143
179
  appendInputToHash(hash, productDir, relative);
@@ -204,17 +204,29 @@ export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.en
204
204
  return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
205
205
  }
206
206
 
207
+ export function buildTemplateExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
208
+ return buildExecutionEnvWithContext(config, null, extraEnv, processEnv, {
209
+ omitRuntimeDatabaseBindings: true,
210
+ });
211
+ }
212
+
207
213
  export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
208
214
  return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
209
215
  }
210
216
 
211
- function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
217
+ function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, options = {}) {
212
218
  const inheritedEnv = { ...processEnv };
213
219
  const templateContext = buildTemplateContext(config, lease);
220
+ const serviceEnv = options.omitRuntimeDatabaseBindings
221
+ ? omitRuntimeDatabaseBindings(config.testkit.serviceEnv || {})
222
+ : config.testkit.serviceEnv || {};
223
+ const localEnv = options.omitRuntimeDatabaseBindings
224
+ ? omitRuntimeDatabaseBindings(config.testkit.local?.env || {})
225
+ : config.testkit.local?.env || {};
214
226
  const env = {
215
227
  ...inheritedEnv,
216
- ...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
217
- ...resolveEnvTemplates(config.testkit.local?.env || {}, templateContext),
228
+ ...resolveEnvTemplates(serviceEnv, templateContext),
229
+ ...resolveEnvTemplates(localEnv, templateContext),
218
230
  ...resolveEnvTemplates(extraEnv, templateContext),
219
231
  TESTKIT_ACTIVE: "1",
220
232
  ...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
@@ -340,6 +352,15 @@ function resolveEnvTemplates(values, templateContext) {
340
352
  );
341
353
  }
342
354
 
355
+ function omitRuntimeDatabaseBindings(values = {}) {
356
+ return Object.fromEntries(
357
+ Object.entries(values).filter(([_key, value]) => {
358
+ if (typeof value !== "string") return true;
359
+ return !/\{db(?:Url|Host|Port|Name|User|Password)(?::[a-zA-Z0-9_-]+)?\}/.test(value);
360
+ })
361
+ );
362
+ }
363
+
343
364
  function finalizeRuntimePrepare(prepare, context) {
344
365
  if (!prepare) {
345
366
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.94"
25
+ "@elench/testkit-protocol": "0.1.95"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -83,10 +83,10 @@
83
83
  },
84
84
  "dependencies": {
85
85
  "@babel/code-frame": "^7.29.0",
86
- "@elench/next-analysis": "0.1.94",
87
- "@elench/testkit-bridge": "0.1.94",
88
- "@elench/testkit-protocol": "0.1.94",
89
- "@elench/ts-analysis": "0.1.94",
86
+ "@elench/next-analysis": "0.1.95",
87
+ "@elench/testkit-bridge": "0.1.95",
88
+ "@elench/testkit-protocol": "0.1.95",
89
+ "@elench/ts-analysis": "0.1.95",
90
90
  "@oclif/core": "^4.10.6",
91
91
  "esbuild": "^0.25.11",
92
92
  "execa": "^9.5.0",