Skip to content

Commit b8c4cfe

Browse files
authored
Fix PHPUnit cache writes through readonly mounts (#1805)
* Fix PHPUnit cache writes through readonly mounts * Restrict PHPUnit caches to sandbox paths * Use private PHPUnit result cache files * Add Playground PHPUnit cache isolation integration * Fix PHPUnit integration workflow triggers
1 parent 57eb74e commit b8c4cfe

7 files changed

Lines changed: 2095 additions & 4 deletions

File tree

.github/workflows/agent-task-contracts.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ on:
2222
- "tests/redaction.test.ts"
2323
- "tests/production-boundary-enforcement.test.ts"
2424
- "tests/runtime-tool-policy.test.ts"
25+
- "packages/runtime-playground/src/phpunit-command-handlers.ts"
26+
- "tests/playground-phpunit-readonly-cache.integration.test.ts"
27+
- "tests/fixtures/phpunit-playground-harness/**"
2528
push:
2629
paths:
2730
- ".github/scripts/run-agent-task/**"
@@ -43,6 +46,9 @@ on:
4346
- "tests/redaction.test.ts"
4447
- "tests/production-boundary-enforcement.test.ts"
4548
- "tests/runtime-tool-policy.test.ts"
49+
- "packages/runtime-playground/src/phpunit-command-handlers.ts"
50+
- "tests/playground-phpunit-readonly-cache.integration.test.ts"
51+
- "tests/fixtures/phpunit-playground-harness/**"
4652

4753
jobs:
4854
contracts:
@@ -55,10 +61,15 @@ jobs:
5561
with:
5662
node-version: 22
5763
cache: npm
64+
- uses: shivammathur/setup-php@v2
65+
with:
66+
php-version: '8.2'
67+
tools: composer:v2
5868
- run: npm ci
5969
- run: npm run build
6070
- run: npm run test:agent-task-contracts
6171
- run: npm run test:runtime-sources-playground-integration
72+
- run: npm run test:playground-phpunit-readonly-cache-integration
6273
- run: npm run test:native-agent-task-playground-e2e
6374
- run: npm run test:native-agent-task-interruption
6475
- run: npm run test:redaction
@@ -71,4 +82,4 @@ jobs:
7182
- uses: actions/checkout@v4
7283
- uses: docker://rhysd/actionlint:1.7.7
7384
with:
74-
args: .github/workflows/run-agent-task.yml
85+
args: .github/workflows/agent-task-contracts.yml .github/workflows/run-agent-task.yml

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"test:runtime-sources-playground-integration": "tsx tests/runtime-sources-playground-integration.test.ts",
9494
"test:playground-readonly-mounts": "tsx tests/playground-readonly-mounts.test.ts",
9595
"test:playground-readonly-mounts-integration": "tsx tests/playground-readonly-mounts-integration.test.ts",
96+
"test:playground-phpunit-readonly-cache-integration": "tsx tests/playground-phpunit-readonly-cache.integration.test.ts",
9697
"test:browser-task-builder": "tsx tests/browser-task-builder.test.ts",
9798
"test:browser-runtime-generic-invoker": "tsx tests/browser-runtime-generic-invoker.test.ts",
9899
"test:browser-runtime-file-ops": "tsx tests/browser-runtime-file-ops.test.ts",

packages/runtime-playground/src/phpunit-command-handlers.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,44 @@ function ${options.relativeFunctionName}(string $path, string $${options.rootPar
273273
}
274274

275275
function phpunitArgsPhp(functionName: string, logFunction: string): string {
276-
return `function ${functionName}(array $argv) {
277-
$arguments = array('colors' => 'never', 'testdox' => true, 'verbose' => false, 'cacheResult' => false, 'extensions' => array());
276+
return `function ${functionName}_private_cache_result_file() {
277+
$path = false;
278+
try {
279+
$candidate = '/tmp/wp-codebox-phpunit-' . bin2hex(random_bytes(24)) . '.cache';
280+
$handle = @fopen($candidate, 'x');
281+
if ($handle !== false) {
282+
fclose($handle);
283+
$path = $candidate;
284+
}
285+
} catch (Throwable $e) {
286+
// tempnam() asks the operating system to allocate a unique file when
287+
// cryptographic randomness is unavailable.
288+
}
289+
if ($path === false) {
290+
$path = tempnam('/tmp', 'wp-codebox-phpunit-');
291+
}
292+
if ($path === false) {
293+
throw new RuntimeException('Unable to allocate a private PHPUnit result cache file.');
294+
}
295+
if (!@chmod($path, 0600)) {
296+
@unlink($path);
297+
throw new RuntimeException('Unable to restrict the private PHPUnit result cache file permissions.');
298+
}
299+
// fileperms() is meaningful on the POSIX filesystems used by Playground.
300+
// Fail closed rather than passing a cache file with broader permissions.
301+
$mode = @fileperms($path);
302+
if ($mode !== false && (($mode & 0777) !== 0600)) {
303+
@unlink($path);
304+
throw new RuntimeException('Private PHPUnit result cache file permissions are not 0600.');
305+
}
306+
register_shutdown_function(static function () use ($path): void {
307+
@unlink($path);
308+
});
309+
return $path;
310+
}
311+
312+
function ${functionName}(array $argv) {
313+
$arguments = array('colors' => 'never', 'testdox' => true, 'verbose' => false, 'cacheResult' => false, 'cacheResultFile' => ${functionName}_private_cache_result_file(), 'extensions' => array());
278314
$args = array_slice($argv, 1);
279315
for ($i = 0; $i < count($args); $i++) {
280316
$arg = $args[$i];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "automattic/wp-codebox-phpunit-playground-harness",
3+
"description": "Locked PHPUnit harness for the WP Codebox Playground integration test.",
4+
"license": "GPL-2.0-or-later",
5+
"require": {
6+
"php": ">=8.1",
7+
"phpunit/phpunit": "9.6.34",
8+
"wp-phpunit/wp-phpunit": "6.9.4",
9+
"yoast/phpunit-polyfills": "3.1.2"
10+
},
11+
"config": {
12+
"platform": {
13+
"php": "8.3.0"
14+
},
15+
"sort-packages": true
16+
}
17+
}

0 commit comments

Comments
 (0)