|
1 | 1 | import assert from "node:assert/strict" |
| 2 | +import { execFile as execFileCallback } from "node:child_process" |
2 | 3 | import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises" |
3 | 4 | import { tmpdir } from "node:os" |
4 | 5 | import { join } from "node:path" |
| 6 | +import { promisify } from "node:util" |
| 7 | +import { recipeRunDependencyOverlay } from "../packages/cli/src/commands/recipe-runtime-setup.js" |
5 | 8 | import { prepareRecipeDependencyOverlays, prepareRecipeRuntimeOverlays } from "../packages/cli/src/recipe-sources.js" |
6 | 9 | import type { PreparedExtraPlugin } from "../packages/cli/src/recipe-sources.js" |
7 | 10 | import type { WorkspaceRecipe } from "../packages/runtime-core/src/runtime-contracts.js" |
8 | 11 |
|
9 | 12 | const root = await mkdtemp(join(tmpdir(), "wp-codebox-runtime-overlay-hydration-")) |
10 | 13 | const overlaySource = join(root, "php-ai-client") |
11 | 14 | const dependencySource = join(root, "generic-composer-package") |
| 15 | +const nonGitDependencySource = join(root, "non-git-composer-package") |
12 | 16 | const binDir = join(root, "bin") |
13 | 17 | const scoperPath = join(root, "php-scoper.phar") |
14 | 18 | const originalPath = process.env.PATH |
15 | 19 | const originalScoper = process.env.WP_CODEBOX_PHP_SCOPER_PHAR |
| 20 | +const execFile = promisify(execFileCallback) |
16 | 21 |
|
17 | 22 | async function exists(path: string): Promise<boolean> { |
18 | 23 | try { |
@@ -51,6 +56,36 @@ await writeFile(join(dependencySource, "composer.json"), JSON.stringify({ |
51 | 56 | autoload: { "psr-4": { "Acme\\Package\\": "src/" } }, |
52 | 57 | require: { "psr/log": "^3.0" }, |
53 | 58 | }, null, 2)) |
| 59 | +await execFile("git", ["init", "--quiet"], { cwd: dependencySource }) |
| 60 | +await execFile("git", ["add", "."], { cwd: dependencySource }) |
| 61 | +await execFile("git", ["-c", "user.name=WP Codebox", "-c", "user.email=wp-codebox@example.test", "commit", "--quiet", "-m", "fixture"], { cwd: dependencySource }) |
| 62 | +const { stdout: dependencyReference } = await execFile("git", ["rev-parse", "HEAD"], { cwd: dependencySource }) |
| 63 | + |
| 64 | +await mkdir(join(nonGitDependencySource, "src"), { recursive: true }) |
| 65 | +await writeFile(join(nonGitDependencySource, "src", "Package.php"), "<?php\n") |
| 66 | +await writeFile(join(nonGitDependencySource, "composer.json"), JSON.stringify({ name: "acme/non-git-package" }, null, 2)) |
| 67 | + |
| 68 | +const consumerSource = join(root, "consumer-plugin") |
| 69 | +await mkdir(join(consumerSource, "vendor", "composer"), { recursive: true }) |
| 70 | +await writeFile(join(consumerSource, "vendor", "composer", "installed.json"), JSON.stringify({ packages: [ |
| 71 | + { name: "acme/package", version: "1.0.0+no-version-set" }, |
| 72 | + { name: "acme/non-git-package", version: "1.0.0+no-version-set" }, |
| 73 | +] }, null, 2)) |
| 74 | +await writeFile(join(consumerSource, "vendor", "composer", "installed.php"), `<?php |
| 75 | +
|
| 76 | +return array( |
| 77 | + 'versions' => array( |
| 78 | + 'acme/package' => array( |
| 79 | + 'pretty_version' => '1.0.0+no-version-set', |
| 80 | + 'reference' => NULL, |
| 81 | + ), |
| 82 | + 'acme/non-git-package' => array( |
| 83 | + 'pretty_version' => '1.0.0+no-version-set', |
| 84 | + 'reference' => NULL, |
| 85 | + ), |
| 86 | + ), |
| 87 | +); |
| 88 | +`) |
54 | 89 |
|
55 | 90 | await writeFile(scoperPath, `<?php |
56 | 91 | $workingDir = ''; |
@@ -116,38 +151,57 @@ const recipe: WorkspaceRecipe = { |
116 | 151 | } |
117 | 152 |
|
118 | 153 | const overlays = await prepareRecipeRuntimeOverlays(recipe, root) |
| 154 | +const consumers: PreparedExtraPlugin[] = [{ |
| 155 | + source: consumerSource, |
| 156 | + slug: "consumer-plugin", |
| 157 | + target: "/wordpress/wp-content/plugins/consumer-plugin", |
| 158 | + pluginFile: "consumer-plugin.php", |
| 159 | + activate: true, |
| 160 | + loadAs: "plugin", |
| 161 | + cleanupPaths: [], |
| 162 | + provenance: { kind: "local", original: consumerSource }, |
| 163 | +}] |
119 | 164 | const dependencyOverlays = await prepareRecipeDependencyOverlays({ |
120 | 165 | inputs: { |
121 | 166 | dependency_overlays: [{ |
122 | 167 | kind: "composer-package", |
123 | 168 | package: "acme/package", |
124 | 169 | source: dependencySource, |
125 | 170 | consumer: "consumer-plugin", |
| 171 | + }, { |
| 172 | + kind: "composer-package", |
| 173 | + package: "acme/non-git-package", |
| 174 | + source: nonGitDependencySource, |
| 175 | + consumer: "consumer-plugin", |
126 | 176 | }], |
127 | 177 | }, |
128 | | -}, root, [{ |
129 | | - source: join(root, "consumer-plugin"), |
130 | | - slug: "consumer-plugin", |
131 | | - target: "/wordpress/wp-content/plugins/consumer-plugin", |
132 | | - pluginFile: "consumer-plugin.php", |
133 | | - activate: true, |
134 | | - loadAs: "plugin", |
135 | | - cleanupPaths: [], |
136 | | - provenance: { kind: "local", original: join(root, "consumer-plugin") }, |
137 | | -}] satisfies PreparedExtraPlugin[]) |
| 178 | +}, root, consumers) |
138 | 179 | try { |
139 | 180 | assert.equal(overlays.length, 1) |
140 | | - assert.equal(dependencyOverlays.length, 1) |
| 181 | + assert.equal(dependencyOverlays.length, 2) |
141 | 182 | assert.equal(await exists(join(overlaySource, "vendor")), false, "overlay source checkout must not be mutated") |
142 | 183 | assert.equal(await exists(join(dependencySource, "vendor")), false, "dependency overlay source checkout must not be mutated") |
143 | 184 | assert.equal(await exists(join(overlays[0].source, "autoload.php")), true) |
144 | 185 | assert.equal(await exists(join(overlays[0].source, "src", "Client.php")), true) |
145 | 186 | assert.equal(await exists(join(overlays[0].source, "third-party", "Psr", "Log", "LoggerInterface.php")), true) |
146 | 187 | assert.equal(await exists(join(dependencyOverlays[0].source, "vendor", "composer", "installed.json")), true) |
147 | 188 | assert.equal(dependencyOverlays[0].target, "/wordpress/wp-content/plugins/consumer-plugin/vendor/acme/package") |
| 189 | + assert.equal(dependencyOverlays[0].reference, dependencyReference.trim(), "clean Git source revision survives Composer staging") |
| 190 | + assert.equal(dependencyOverlays[0].metadata.reference, dependencyReference.trim(), "mounted dependency metadata preserves the source revision") |
| 191 | + assert.equal(recipeRunDependencyOverlay(dependencyOverlays[0]).reference, dependencyReference.trim(), "runtime dependency provenance exposes the source revision") |
| 192 | + assert.equal(dependencyOverlays[1].reference, undefined, "non-Git source has no fabricated revision") |
| 193 | + assert.equal(dependencyOverlays[1].metadata.reference, undefined, "non-Git source metadata omits the revision") |
| 194 | + assert.equal((JSON.parse(await readFile(join(consumerSource, "vendor", "composer", "installed.json"), "utf8")) as { packages: Array<{ source?: unknown }> }).packages[0].source, undefined, "original consumer Composer provenance remains unchanged") |
| 195 | + const runtimeInstalled = JSON.parse(await readFile(join(consumers[0].source, "vendor", "composer", "installed.json"), "utf8")) as { packages: Array<{ name: string, version: string, source?: { reference?: string } }> } |
| 196 | + assert.deepEqual(runtimeInstalled.packages[0], { name: "acme/package", version: "1.0.0+no-version-set", source: { reference: dependencyReference.trim() } }, "runtime Composer dependency provenance includes the immutable source reference") |
| 197 | + assert.deepEqual(runtimeInstalled.packages[1], { name: "acme/non-git-package", version: "1.0.0+no-version-set" }, "runtime Composer provenance omits unresolved source references") |
| 198 | + const { stdout: runtimeInstalledPhp } = await execFile("php", ["-r", "echo json_encode(require $argv[1]);", join(consumers[0].source, "vendor", "composer", "installed.php")]) |
| 199 | + const runtimePhpVersions = JSON.parse(runtimeInstalledPhp) as { versions: Record<string, { reference?: string | null }> } |
| 200 | + assert.equal(runtimePhpVersions.versions["acme/package"]?.reference, dependencyReference.trim(), "Composer runtime metadata includes the immutable source reference") |
| 201 | + assert.equal(runtimePhpVersions.versions["acme/non-git-package"]?.reference, null, "Composer runtime metadata leaves unresolved references unchanged") |
148 | 202 | assert.match(await readFile(join(overlays[0].source, "src", "Client.php"), "utf8"), /WordPress\\AiClientDependencies\\Psr\\Log\\LoggerInterface/) |
149 | 203 | } finally { |
150 | | - await Promise.all([...overlays, ...dependencyOverlays].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true }))) |
| 204 | + await Promise.all([...overlays, ...dependencyOverlays, ...consumers].flatMap((overlay) => overlay.cleanupPaths).map((path) => rm(path, { recursive: true, force: true }))) |
151 | 205 | process.env.PATH = originalPath |
152 | 206 | if (originalScoper === undefined) { |
153 | 207 | delete process.env.WP_CODEBOX_PHP_SCOPER_PHAR |
|
0 commit comments