Files
promptfoo--promptfoo/test/examples/integrationInspectOsworldProvider.test.ts
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

1008 lines
30 KiB
TypeScript

import { spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { PythonProvider } from '../../src/providers/pythonCompletion';
import { mockProcessEnv } from '../util/utils';
describe('integration-inspect-osworld example provider', () => {
const tempDirs: string[] = [];
let restoreEnv: (() => void) | undefined;
let cachedPythonExecutable: string | undefined;
afterEach(() => {
restoreEnv?.();
restoreEnv = undefined;
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true });
}
});
function makeTempDir(): string {
const root = path.join(process.cwd(), 'scratch', 'osworld-provider-tests');
fs.mkdirSync(root, { recursive: true });
const dir = fs.mkdtempSync(path.join(root, 'case-'));
tempDirs.push(dir);
return dir;
}
function pythonExecutable(): string {
if (cachedPythonExecutable) {
return cachedPythonExecutable;
}
const pythonProbe = 'import sys; print(sys.executable)';
const candidates = [
...(process.env.PROMPTFOO_PYTHON
? [{ command: process.env.PROMPTFOO_PYTHON, args: ['-c', pythonProbe] }]
: []),
...(process.env.PYTHON ? [{ command: process.env.PYTHON, args: ['-c', pythonProbe] }] : []),
...(process.env.PYTHON3 ? [{ command: process.env.PYTHON3, args: ['-c', pythonProbe] }] : []),
...(process.platform === 'win32'
? [
{ command: 'py', args: ['-3', '-c', pythonProbe] },
{ command: 'python', args: ['-c', pythonProbe] },
]
: [
{ command: 'python3', args: ['-c', pythonProbe] },
{ command: 'python', args: ['-c', pythonProbe] },
]),
];
for (const candidate of candidates) {
const result = spawnSync(candidate.command, candidate.args, { encoding: 'utf8' });
const executable = result.status === 0 ? result.stdout.trim() : '';
if (executable) {
cachedPythonExecutable = executable;
return executable;
}
}
return process.platform === 'win32' ? 'python' : 'python3';
}
it('generates the Inspect osworld_small sample suite', () => {
const tempDir = makeTempDir();
const fakeInspectRoot = path.join(tempDir, 'fake-inspect');
const fakeOsworldDir = path.join(fakeInspectRoot, 'inspect_evals', 'osworld');
fs.mkdirSync(fakeOsworldDir, { recursive: true });
fs.writeFileSync(path.join(fakeInspectRoot, 'inspect_evals', '__init__.py'), '');
fs.writeFileSync(
path.join(fakeOsworldDir, '__init__.py'),
`
from types import SimpleNamespace
APP_DIRS = [
"gimp",
"gimp",
"libreoffice_calc",
"libreoffice_calc",
"libreoffice_calc",
"libreoffice_impress",
"libreoffice_impress",
"libreoffice_writer",
"libreoffice_writer",
"multi_apps",
"multi_apps",
"multi_apps",
"multi_apps",
"multi_apps",
"multi_apps",
"os",
"os",
"vlc",
"vs_code",
"vs_code",
"vs_code",
]
FULL_APP_DIRS = APP_DIRS + ["chrome", "thunderbird"]
class FakeSample:
def __init__(self, index, app_dir):
sample_id = f"{index:08d}-0000-4000-8000-000000000000"
self.id = sample_id
self.input = f"Instruction {index} for {app_dir}"
self.files = {
"/tmp/osworld/desktop_env/example.json": (
f"/cache/osworld/repo/evaluation_examples/examples/{app_dir}/{sample_id}.json"
)
}
def _samples():
return [FakeSample(index, app_dir) for index, app_dir in enumerate(APP_DIRS, start=1)]
def osworld_small():
return SimpleNamespace(dataset=_samples())
def osworld(include_connected=False):
return SimpleNamespace(
dataset=[FakeSample(index, app_dir) for index, app_dir in enumerate(FULL_APP_DIRS, start=1)]
)
`,
);
const testsPath = path.join(
process.cwd(),
'examples',
'integration-inspect-osworld',
'osworld_tests.py',
);
const checkPath = path.join(tempDir, 'check_osworld_tests.py');
fs.writeFileSync(
checkPath,
`
import importlib.util
import json
import sys
from pathlib import Path
tests_path = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("osworld_tests", tests_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
tests = module.generate_tests()
full_tests = module.generate_full_tests()
print(json.dumps({
"count": len(tests),
"full_count": len(full_tests),
"apps": sorted({test["vars"]["app"] for test in tests}),
"full_apps": sorted({test["vars"]["app"] for test in full_tests}),
"vscode_count": sum(1 for test in tests if test["vars"]["app"] == "vscode"),
"first": tests[0],
}))
`,
);
const result = spawnSync(pythonExecutable(), [checkPath, testsPath], {
encoding: 'utf8',
env: {
...process.env,
PYTHONPATH: [fakeInspectRoot, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter),
},
});
expect(result.status).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.count).toBe(21);
expect(parsed.full_count).toBe(23);
expect(parsed.apps).toEqual([
'gimp',
'libreoffice_calc',
'libreoffice_impress',
'libreoffice_writer',
'multi_apps',
'os',
'vlc',
'vscode',
]);
expect(parsed.full_apps).toEqual([
'chrome',
'gimp',
'libreoffice_calc',
'libreoffice_impress',
'libreoffice_writer',
'multi_apps',
'os',
'thunderbird',
'vlc',
'vscode',
]);
expect(parsed.vscode_count).toBe(3);
expect(parsed.first).toMatchObject({
description: 'gimp - Instruction 1 for gimp',
vars: {
app: 'gimp',
prompt: 'Instruction 1 for gimp',
sample_id: '00000001-0000-4000-8000-000000000000',
},
metadata: {
app: 'gimp',
sample_id: '00000001-0000-4000-8000-000000000000',
testCaseId: 'osworld-gimp-00000001',
},
});
});
it('creates unique log dirs for repeated same-app calls in one timestamp', () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
const providerPath = path.join(providerDir, 'provider.py');
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
providerPath,
);
const checkPath = path.join(tempDir, 'check_log_dirs.py');
fs.writeFileSync(
checkPath,
`
import importlib.util
import json
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
provider_path = Path(sys.argv[1])
log_root = Path(sys.argv[2])
spec = importlib.util.spec_from_file_location("provider", provider_path)
provider = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider)
class FrozenDateTime:
@staticmethod
def now(tz=None):
return datetime(2026, 1, 2, 3, 4, 5, tzinfo=tz)
provider.datetime = FrozenDateTime
uuid_values = iter([
"aaaaaaaa000000000000000000000000",
"bbbbbbbb000000000000000000000000",
])
provider.uuid.uuid4 = lambda: SimpleNamespace(hex=next(uuid_values))
first = provider._new_log_dir(log_root, "libreoffice_calc")
second = provider._new_log_dir(log_root, "libreoffice_calc")
print(json.dumps({
"first": first.name,
"second": second.name,
"count": len([path for path in log_root.iterdir() if path.name.startswith("20260102T030405Z-")]),
}))
`,
);
const result = spawnSync(pythonExecutable(), [checkPath, providerPath, tempDir], {
encoding: 'utf8',
});
expect(result.status).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.first).not.toBe(parsed.second);
expect(parsed.first).toMatch(/^20260102T030405Z-libreoffice_calc-\d+-aaaaaaaa$/);
expect(parsed.second).toMatch(/^20260102T030405Z-libreoffice_calc-\d+-bbbbbbbb$/);
expect(parsed.count).toBe(2);
});
it('prefers osworld_scorer and treats missing scorer results as provider errors', () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
const providerPath = path.join(providerDir, 'provider.py');
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
providerPath,
);
const checkPath = path.join(tempDir, 'check_scores.py');
fs.writeFileSync(
checkPath,
`
import importlib.util
import json
import sys
from pathlib import Path
provider_path = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("provider", provider_path)
provider = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider)
preferred, preferred_error = provider._sample_score({
"scores": {
"aux_metric": {"value": 1.0},
"osworld_scorer": {"value": 0.0},
}
})
missing_scorer, missing_scorer_error = provider._sample_score({
"scores": {
"aux_metric": {"value": 1.0},
}
})
aggregate_only = provider._parse_inspect_log({
"status": "success",
"samples": [{
"id": "sample-123",
"scores": {},
}],
"results": {
"scores": [{
"metrics": {"accuracy": {"value": 1.0}},
}],
},
})
missing_error = provider._inspect_log_error({
"score": None,
"sample_id": "sample-123",
"inspect_status": "success",
})
print(json.dumps({
"preferred": preferred,
"preferred_error": preferred_error,
"missing_scorer": missing_scorer,
"missing_scorer_error": missing_scorer_error,
"aggregate_only_score": aggregate_only["score"],
"missing_error": missing_error,
}))
`,
);
const result = spawnSync(pythonExecutable(), [checkPath, providerPath], {
encoding: 'utf8',
});
expect(result.status).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.preferred).toBe(0);
expect(parsed.preferred_error).toBeNull();
expect(parsed.missing_scorer).toBeNull();
expect(parsed.missing_scorer_error).toBe(
'Inspect did not record osworld_scorer for the selected sample.',
);
expect(parsed.aggregate_only_score).toBeNull();
expect(parsed.missing_error).toBe(
'Inspect completed sample sample-123, but no OSWorld scorer result was recorded.',
);
});
it('summarizes unscored Inspect sample errors without returning raw tracebacks', () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
const providerPath = path.join(providerDir, 'provider.py');
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
providerPath,
);
const checkPath = path.join(tempDir, 'check_error_summaries.py');
fs.writeFileSync(
checkPath,
`
import importlib.util
import json
import sys
from pathlib import Path
provider_path = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("provider", provider_path)
provider = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider)
print(json.dumps({
"computer_tool": provider._summarize_inspect_error(
"Failure executing command: pyautogui.click(...)\\ntraceback"
),
"missing_image": provider._summarize_inspect_error(
"FileNotFoundError: /tmp/osworld/cache/image_original.png\\ntraceback"
),
"missing_vm_ip": provider._summarize_inspect_error(
"AttributeError: 'OSWorldDesktopEnv' object has no attribute 'vm_ip'\\ntraceback"
),
"generic": provider._summarize_inspect_error(
"very long secret-ish scorer traceback\\nsecond line"
),
}))
`,
);
const result = spawnSync(pythonExecutable(), [checkPath, providerPath], {
encoding: 'utf8',
});
expect(result.status).toBe(0);
const parsed = JSON.parse(result.stdout);
expect(parsed.computer_tool).toBe(
'Inspect computer tool failed while executing a model-requested desktop command. See the Inspect log for the full traceback and command output.',
);
expect(parsed.missing_image).toBe(
'OSWorld scorer could not read an expected image artifact. See the Inspect log for the full traceback.',
);
expect(parsed.missing_vm_ip).toBe(
'OSWorld scorer could not read VLC desktop state. See the Inspect log for the full traceback.',
);
expect(parsed.generic).toBe(
'Inspect reported an unscored sample error. See the Inspect log for details.',
);
});
it('rejects empty Inspect commands before launching Inspect', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: ' ',
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { sample_id: 'sample-123' },
} as any);
expect(result.error).toBe(
'Inspect CLI command is empty. Set providers[0].config.inspectCommand or PROMPTFOO_OSWORLD_INSPECT_COMMAND.',
);
} finally {
await provider.shutdown();
}
}, 20_000);
it('finds Inspect logs when promptfoo passes a relative basePath', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
const recordPath = path.join(tempDir, 'inspect_eval_record.json');
fs.writeFileSync(
fakeInspectPath,
`
import json
import os
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
eval_log = Path(args[2])
if not eval_log.exists():
print(f"missing eval log: {eval_log}", file=sys.stderr)
sys.exit(2)
print(json.dumps({
"status": "success",
"samples": [{
"id": "sample-123",
"scores": {"osworld_scorer": {"value": "C"}},
"output": {"completion": "DONE"},
"messages": [{"role": "assistant", "content": "DONE"}],
"model_usage": {"mock/model": {"input_tokens": 10, "output_tokens": 3}},
}],
"results": {"scores": [{"metrics": {"accuracy": {"value": 1.0}}}]},
}))
sys.exit(0)
if args and args[0] == "eval":
log_dir = Path(args[args.index("--log-dir") + 1])
Path(os.environ["FAKE_INSPECT_RECORD"]).write_text(json.dumps({
"args": args,
"cwd": os.getcwd(),
"log_dir": str(log_dir),
"is_absolute": log_dir.is_absolute(),
}))
if not log_dir.is_absolute():
print(f"log dir should be absolute, got: {log_dir}", file=sys.stderr)
sys.exit(3)
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
print(f"unexpected args: {args}", file=sys.stderr)
sys.exit(4)
`,
);
restoreEnv = mockProcessEnv({ FAKE_INSPECT_RECORD: recordPath });
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { app: 'libreoffice_calc', sample_id: 'sample-123' },
} as any);
expect(result.error).toBeUndefined();
expect(result.output).toContain('sample-123');
expect(result.metadata).toMatchObject({
app: 'libreoffice_calc',
model: 'mock/model',
sample_id: 'sample-123',
score: 1,
status: 'pass',
});
expect(result.tokenUsage).toMatchObject({ completion: 3, prompt: 10, total: 13 });
const inspectLogPath = String(result.metadata?.inspect_log_path);
expect(path.isAbsolute(inspectLogPath)).toBe(true);
expect(path.normalize(inspectLogPath)).toContain(path.join('example', 'inspect_logs'));
expect(path.normalize(inspectLogPath)).not.toContain(
path.join('example', 'example', 'inspect_logs'),
);
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
expect(record.is_absolute).toBe(true);
expect(record.args).toContain('--sample-id');
expect(record.args).toContain('sample-123');
expect(path.resolve(record.cwd)).toBe(path.resolve(providerDir));
} finally {
await provider.shutdown();
}
}, 20_000);
it('selects exact OSWorld samples by sample_id without app filters', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
const recordPath = path.join(tempDir, 'inspect_eval_record.json');
fs.writeFileSync(
fakeInspectPath,
`
import json
import os
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
print(json.dumps({
"status": "success",
"samples": [{
"id": "exact-sample-123",
"scores": {"osworld_scorer": {"value": "C"}},
"output": {"completion": "DONE"},
"messages": [{"role": "assistant", "content": "DONE"}],
}],
}))
sys.exit(0)
if args and args[0] == "eval":
Path(os.environ["FAKE_INSPECT_RECORD"]).write_text(json.dumps({"args": args}))
log_dir = Path(args[args.index("--log-dir") + 1])
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
sys.exit(4)
`,
);
restoreEnv = mockProcessEnv({ FAKE_INSPECT_RECORD: recordPath });
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { sample_id: 'exact-sample-123' },
} as any);
expect(result.error).toBeUndefined();
expect(result.metadata).toMatchObject({
app: 'unknown',
requested_sample_id: 'exact-sample-123',
sample_id: 'exact-sample-123',
score: 1,
status: 'pass',
});
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
expect(record.args).toContain('--sample-id');
expect(record.args).toContain('exact-sample-123');
expect(record.args).not.toContain('--limit');
expect(record.args).not.toContain('-T');
} finally {
await provider.shutdown();
}
}, 20_000);
it('passes configured Inspect task parameters to full-suite runs', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
const recordPath = path.join(tempDir, 'inspect_eval_record.json');
fs.writeFileSync(
fakeInspectPath,
`
import json
import os
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
print(json.dumps({
"status": "success",
"samples": [{
"id": "sample-123",
"scores": {"osworld_scorer": {"value": "C"}},
"output": {"completion": "DONE"},
"messages": [{"role": "assistant", "content": "DONE"}],
}],
}))
sys.exit(0)
if args and args[0] == "eval":
Path(os.environ["FAKE_INSPECT_RECORD"]).write_text(json.dumps({"args": args}))
log_dir = Path(args[args.index("--log-dir") + 1])
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
sys.exit(4)
`,
);
restoreEnv = mockProcessEnv({ FAKE_INSPECT_RECORD: recordPath });
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
task: 'inspect_evals/osworld',
taskParameters: {
include_connected: true,
max_attempts: 2,
},
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { sample_id: 'sample-123' },
} as any);
expect(result.error).toBeUndefined();
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
expect(record.args).toEqual(
expect.arrayContaining([
'eval',
'inspect_evals/osworld',
'-T',
'include_connected=true',
'-T',
'max_attempts=2',
]),
);
} finally {
await provider.shutdown();
}
}, 20_000);
it('returns a provider error when Inspect returns the wrong sample id', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
fs.writeFileSync(
fakeInspectPath,
`
import json
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
print(json.dumps({
"status": "success",
"samples": [{
"id": "other-sample",
"scores": {"osworld_scorer": {"value": "C"}},
"messages": [],
}],
}))
sys.exit(0)
if args and args[0] == "eval":
log_dir = Path(args[args.index("--log-dir") + 1])
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
sys.exit(4)
`,
);
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { app: 'libreoffice_calc', sample_id: 'requested-sample' },
} as any);
expect(result.error).toBe(
'Inspect returned sample other-sample, but Promptfoo requested requested-sample.',
);
expect(result.metadata).toMatchObject({
requested_sample_id: 'requested-sample',
sample_id: 'other-sample',
status: 'error',
});
} finally {
await provider.shutdown();
}
}, 20_000);
it('treats zero selected Inspect samples as provider errors', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
fs.writeFileSync(
fakeInspectPath,
`
import json
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
print(json.dumps({
"status": "success",
"samples": [],
"results": {"scores": []},
}))
sys.exit(0)
if args and args[0] == "eval":
log_dir = Path(args[args.index("--log-dir") + 1])
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
sys.exit(4)
`,
);
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { sample_id: 'missing-sample' },
} as any);
expect(result.error).toContain('selected zero samples');
expect(result.metadata).toMatchObject({
app: 'unknown',
inspect_error:
'Inspect completed, but selected zero samples or recorded no scorer result. Check vars.sample_id.',
sample_id: 'unknown sample',
status: 'error',
});
} finally {
await provider.shutdown();
}
}, 20_000);
it('does not persist captured Inspect failure output or command secrets', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
fs.writeFileSync(
fakeInspectPath,
`
import sys
print("sensitive stderr sekret", file=sys.stderr)
print("sensitive stdout sekret")
sys.exit(7)
`,
);
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath, '--api-key', 'sekret'],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { sample_id: 'sample-123' },
} as any);
expect(result.error).toBe('Inspect eval failed with exit code 7.');
expect(result.error).not.toContain('sekret');
expect(result.metadata).toMatchObject({ status: 'error' });
expect(result.metadata).not.toHaveProperty('stdout_tail');
expect(result.metadata).not.toHaveProperty('stderr_tail');
} finally {
await provider.shutdown();
}
}, 20_000);
it('condenses long Inspect computer command failures', () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
const providerPath = path.join(providerDir, 'provider.py');
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
providerPath,
);
const checkPath = path.join(tempDir, 'check_error_summary.py');
fs.writeFileSync(
checkPath,
`
import importlib.util
import sys
from pathlib import Path
provider_path = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("provider", provider_path)
provider = importlib.util.module_from_spec(spec)
spec.loader.exec_module(provider)
print(provider._summarize_inspect_error("RuntimeError('Failure executing command: $['python3', 'tool.py', '--text=' + 'x' * 5000]')"))
`,
);
const result = spawnSync(pythonExecutable(), [checkPath, providerPath], {
encoding: 'utf8',
});
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe(
'Inspect computer tool failed while executing a model-requested desktop command. See the Inspect log for the full traceback and command output.',
);
});
it('surfaces Inspect log runtime errors as provider errors', async () => {
const tempDir = makeTempDir();
const providerDir = path.join(tempDir, 'example');
fs.mkdirSync(providerDir, { recursive: true });
fs.copyFileSync(
path.join(process.cwd(), 'examples', 'integration-inspect-osworld', 'provider.py'),
path.join(providerDir, 'provider.py'),
);
const fakeInspectPath = path.join(tempDir, 'fake_inspect.py');
fs.writeFileSync(
fakeInspectPath,
`
import json
import sys
from pathlib import Path
args = sys.argv[1:]
if args[:2] == ["log", "dump"]:
print(json.dumps({
"status": "error",
"samples": [{
"id": "sample-error",
"scores": {},
"error": {"message": "computer tool rejected wait duration"},
"messages": [],
"model_usage": {"mock/model": {"input_tokens": 4, "output_tokens": 2}},
}],
}))
sys.exit(0)
if args and args[0] == "eval":
log_dir = Path(args[args.index("--log-dir") + 1])
log_dir.mkdir(parents=True, exist_ok=True)
(log_dir / "fake.eval").write_text("fake eval")
sys.exit(0)
sys.exit(4)
`,
);
const provider = new PythonProvider('file://provider.py', {
config: {
basePath: path.relative(process.cwd(), providerDir),
defaultModel: 'mock/model',
inspectCommand: [pythonExecutable(), fakeInspectPath],
pythonExecutable: pythonExecutable(),
timeout: 10_000,
timeoutSeconds: 10,
},
});
try {
const result = await provider.callApi('ignored', {
vars: { app: 'libreoffice_calc', sample_id: 'sample-error' },
} as any);
expect(result.error).toContain(
'Inspect reported an unscored sample error. See the Inspect log for details.',
);
expect(result.metadata).toMatchObject({
inspect_error:
'Inspect reported an unscored sample error. See the Inspect log for details.',
inspect_status: 'error',
sample_id: 'sample-error',
status: 'error',
});
expect(result.tokenUsage).toMatchObject({ completion: 2, prompt: 4, total: 6 });
} finally {
await provider.shutdown();
}
}, 20_000);
});