diff --git a/plugins/cua/plugin.json b/plugins/cua/plugin.json index d5e121944..d60954146 100644 --- a/plugins/cua/plugin.json +++ b/plugins/cua/plugin.json @@ -31,7 +31,7 @@ "integrityDescriptor": "runtime/${target.platform}/${arch}/integrity.json", "adapterContract": { "hostBundleId": "com.wefonk.deepchat", - "driverVersion": "0.17.0", + "driverVersion": "0.19.2", "contractVersion": "0.6.0", "toolsListSchemaVersion": "1", "capabilityVersion": "1", @@ -47,7 +47,7 @@ "mode": "user-confirmed", "provider": "trycua", "strategy": "bundled-plugin-helper", - "minVersion": "0.17.0", + "minVersion": "0.19.2", "guideUrl": "https://cua.ai/docs/cua-driver/guide/getting-started/installation" } }, diff --git a/plugins/cua/vendor/cua-driver/upstream.json b/plugins/cua/vendor/cua-driver/upstream.json index b4701b7c3..4f6c8c39a 100644 --- a/plugins/cua/vendor/cua-driver/upstream.json +++ b/plugins/cua/vendor/cua-driver/upstream.json @@ -2,35 +2,35 @@ "sourceKind": "upstream-release", "upstreamRepo": "https://github.com/trycua/cua.git", "upstreamSubdir": "libs/cua-driver/rust", - "tag": "cua-driver-rs-v0.17.0", - "commit": "10279552e2bbe479e367a082f78b1b98ee85a697", - "version": "0.17.0", - "updatedAt": "2026-08-03", - "releaseUrl": "https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.17.0", + "tag": "cua-driver-rs-v0.19.2", + "commit": "20bb34b16ad7c6c56221c332e46b1875e9d8af8c", + "version": "0.19.2", + "updatedAt": "2026-08-07", + "releaseUrl": "https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.19.2", "checksumsAsset": "checksums.txt", - "checksumsSha256": "a3d58c35df49b35c63135a5b969879a7bd831f7d59d9d76b44c6c341ce1bfd7f", + "checksumsSha256": "2aa497943793980bba915ebd6ebfab3aae9b7837064464055804862fd03068b4", "supportedTargets": ["darwin/arm64", "darwin/x64", "win32/x64", "win32/arm64", "linux/x64"], "unsupportedTargets": ["linux/arm64"], "assets": { "darwin-arm64": { - "name": "cua-driver-rs-0.17.0-darwin-arm64.tar.gz", - "sha256": "55ed672850492080ff4e7dab4948b4f3bc70c3b84884a89b1eb8521a5d8177a8" + "name": "cua-driver-rs-0.19.2-darwin-arm64.tar.gz", + "sha256": "c30a81f6b5cfd44d40653f7549d7d714b445e9cbd0ed012c4c524f1c43d2872b" }, "darwin-x64": { - "name": "cua-driver-rs-0.17.0-darwin-x86_64.tar.gz", - "sha256": "cbabf2f71a25b6b158a42384b775d3a75fac15185280d2036d175ee8b49301ae" + "name": "cua-driver-rs-0.19.2-darwin-x86_64.tar.gz", + "sha256": "a9c0ae771f984077dfaddde50f340e52e8b9919f2b8f5917f44334bce92e92de" }, "windows-x64": { - "name": "cua-driver-rs-0.17.0-windows-x86_64-binary.zip", - "sha256": "f7e366edc4b7148b4f6f78957782b2a2d962620b0daaeb99df7cf9dce6176193" + "name": "cua-driver-rs-0.19.2-windows-x86_64-binary.zip", + "sha256": "9868b60999e64ed1028a0f65082624dab7523b06f33b68d582fa0a187d1bf618" }, "windows-arm64": { - "name": "cua-driver-rs-0.17.0-windows-arm64-binary.zip", - "sha256": "bd3febdabff06331efd0951495f34ef7a5fb2cc230fd5270bd34292bc7ee036a" + "name": "cua-driver-rs-0.19.2-windows-arm64-binary.zip", + "sha256": "3976148f985912d03c1876766284898dfbe672db5cc2daa2fb03a0ccb755a1fd" }, "linux-x64": { - "name": "cua-driver-rs-0.17.0-linux-x86_64-binary.tar.gz", - "sha256": "d8fc3e69fc83f01e6c3a70acef1b16e59a2dfc3c91aa98276360a6d211a61017" + "name": "cua-driver-rs-0.19.2-linux-x86_64-binary.tar.gz", + "sha256": "13372e584fcfe4020fe0c5a70ac8d4f0e1c37125f104702d0156d0dddf98fc15" } }, "releasePolicy": "Stage pinned upstream release assets at build time. Do not run upstream installers or require PATH-installed runtime binaries for the bundled DeepChat plugin." diff --git a/scripts/build-cua-plugin-runtime.mjs b/scripts/build-cua-plugin-runtime.mjs index f80b18bd7..22a2fa66a 100644 --- a/scripts/build-cua-plugin-runtime.mjs +++ b/scripts/build-cua-plugin-runtime.mjs @@ -16,6 +16,7 @@ import { } from './cua-macos-contract.mjs' import { signMacHelper } from './sign-cua-helper.mjs' import { parseCuaToolCatalog } from './cua-tool-catalog-contract.mjs' +import { writeCuaRuntimeIntegrityDescriptor } from './package-plugin.mjs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootDir = process.env.DEEPCHAT_ROOT_DIR @@ -675,12 +676,12 @@ export function enforceDarwinLoadPathContract( async function signDarwinHelper(runtimeDir, targetPlatform, packagePurpose) { if (targetPlatform !== 'darwin' || process.platform !== 'darwin') { - return + return null } ensureTool('codesign', ['--version']) const helperAppPath = path.join(runtimeDir, darwinHelperAppDirName) const entitlementsPath = path.join(pluginDir, 'build', 'entitlements.plist') - await signMacHelper({ + return signMacHelper({ appPath: helperAppPath, entitlementsPath, purpose: packagePurpose, @@ -734,19 +735,27 @@ async function main() { if (targetPlatform === 'darwin' && process.platform === 'darwin') { enforceDarwinLoadPathContract(executable) } - await signDarwinHelper(runtimeDir, targetPlatform, packagePurpose) + const signingResult = await signDarwinHelper(runtimeDir, targetPlatform, packagePurpose) smokeCheck(executable, targetPlatform, targetArch) await generateCuaToolCatalog( executable, path.join(runtimeDir, 'tool-catalog.json'), metadata.version ) - - const relativeRuntimePath = path.relative(rootDir, runtimeDir) const stat = await fs.stat(executable) if (stat.size === 0) { throw new Error('Staged CUA runtime is invalid') } + const { descriptorPath } = writeCuaRuntimeIntegrityDescriptor(pluginDir, { + targetPlatform, + targetArch, + purpose: signingResult?.purpose ?? packagePurpose + }) + console.log( + `Generated CUA runtime integrity descriptor: ${path.relative(rootDir, descriptorPath)}` + ) + + const relativeRuntimePath = path.relative(rootDir, runtimeDir) console.log(`CUA Driver ${metadata.tag} staged at ${relativeRuntimePath}`) } catch (error) { buildError = error diff --git a/scripts/package-plugin.mjs b/scripts/package-plugin.mjs index 47f80b788..07e91aa50 100644 --- a/scripts/package-plugin.mjs +++ b/scripts/package-plugin.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -import { createHash } from 'node:crypto' +import { createHash, randomUUID } from 'node:crypto' +import { pathToFileURL } from 'node:url' import { zipSync } from 'fflate' import { readCuaToolCatalog } from './cua-tool-catalog-contract.mjs' import { CUA_DARWIN_ALLOWED_ENTITLEMENTS } from './cua-macos-contract.mjs' @@ -14,7 +15,7 @@ const CUA_PLUGIN_ID = 'com.deepchat.plugins.cua' const CUA_INTEGRITY_DESCRIPTOR_NAME = 'integrity.json' const CUA_EMBEDDED_ADAPTER_CONTRACT = Object.freeze({ hostBundleId: 'com.wefonk.deepchat', - driverVersion: '0.17.0', + driverVersion: '0.19.2', contractVersion: '0.6.0', toolsListSchemaVersion: '1', capabilityVersion: '1', @@ -654,6 +655,60 @@ export function createCuaRuntimeIntegrityDescriptor(files, manifest, args) { } } +export function writeCuaRuntimeIntegrityDescriptor(pluginDir, args) { + const sourceManifest = readManifest(pluginDir) + const manifest = createPackageManifest(sourceManifest, { + version: sourceManifest.version, + releaseVersionFromRoot: false, + targetPlatform: args.targetPlatform, + targetArch: args.targetArch + }) + const files = collectFiles(pluginDir, pluginDir, {}, manifest, args) + const descriptor = createCuaRuntimeIntegrityDescriptor(files, manifest, args) + const descriptorRelativePath = assertSafeRelativePath( + manifest.runtime.integrityDescriptor, + 'CUA runtime integrity descriptor' + ) + const descriptorPath = path.join(pluginDir, ...descriptorRelativePath.split('/')) + const temporaryPath = path.join( + path.dirname(descriptorPath), + `.${path.basename(descriptorPath)}-${randomUUID()}.tmp` + ) + const backupPath = `${temporaryPath}.backup` + fs.mkdirSync(path.dirname(descriptorPath), { recursive: true }) + try { + fs.writeFileSync(temporaryPath, `${JSON.stringify(descriptor, null, 2)}\n`, { flag: 'wx' }) + try { + fs.renameSync(temporaryPath, descriptorPath) + } catch (error) { + if ( + !(error instanceof Error) || + !['EEXIST', 'EPERM'].includes(error.code) + ) { + throw error + } + fs.renameSync(descriptorPath, backupPath) + try { + fs.renameSync(temporaryPath, descriptorPath) + } catch (replacementError) { + try { + fs.renameSync(backupPath, descriptorPath) + } catch (restoreError) { + throw new AggregateError( + [replacementError, restoreError], + 'CUA runtime integrity descriptor replacement and rollback failed' + ) + } + throw replacementError + } + fs.rmSync(backupPath, { force: true }) + } + } finally { + fs.rmSync(temporaryPath, { force: true }) + } + return { descriptor, descriptorPath } +} + function packagePlugin(pluginDir, outDir, manifest, args) { const files = collectFiles(pluginDir, pluginDir, {}, manifest, args) files['plugin.json'] = { @@ -686,22 +741,30 @@ function packagePlugin(pluginDir, outDir, manifest, args) { return outPath } -try { - const args = parseArgs(process.argv.slice(2)) - const sourceManifest = readManifest(args.pluginDir) - const manifest = createPackageManifest(sourceManifest, args) - validateManifest(args.pluginDir, manifest) - if (!isManifestTargetSupported(manifest, args.targetPlatform, args.targetArch)) { - throw new Error(`Plugin ${manifest.id} does not support ${targetKey(args.targetPlatform, args.targetArch)}`) - } - scopeCuaToolPolicyToTarget(args.pluginDir, manifest, args) - validateCuaRuntime(args.pluginDir, manifest, args) - if (args.validateOnly) { - console.log(`Plugin ${manifest.id}@${manifest.version} is valid`) - } else { - const outPath = packagePlugin(args.pluginDir, args.outDir, manifest, args) - console.log(`Packaged ${manifest.id}@${manifest.version}: ${outPath}`) - } -} catch (error) { - fail(error instanceof Error ? error.message : String(error)) +function main() { + try { + const args = parseArgs(process.argv.slice(2)) + const sourceManifest = readManifest(args.pluginDir) + const manifest = createPackageManifest(sourceManifest, args) + validateManifest(args.pluginDir, manifest) + if (!isManifestTargetSupported(manifest, args.targetPlatform, args.targetArch)) { + throw new Error( + `Plugin ${manifest.id} does not support ${targetKey(args.targetPlatform, args.targetArch)}` + ) + } + scopeCuaToolPolicyToTarget(args.pluginDir, manifest, args) + validateCuaRuntime(args.pluginDir, manifest, args) + if (args.validateOnly) { + console.log(`Plugin ${manifest.id}@${manifest.version} is valid`) + } else { + const outPath = packagePlugin(args.pluginDir, args.outDir, manifest, args) + console.log(`Packaged ${manifest.id}@${manifest.version}: ${outPath}`) + } + } catch (error) { + fail(error instanceof Error ? error.message : String(error)) + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main() } diff --git a/src/main/plugin/cuaToolAdapter.ts b/src/main/plugin/cuaToolAdapter.ts index 58da2d61b..e83db11b5 100644 --- a/src/main/plugin/cuaToolAdapter.ts +++ b/src/main/plugin/cuaToolAdapter.ts @@ -79,7 +79,7 @@ const CUA_DEGRADED_REASON_CODES = [ const CUA_WINDOW_ESCALATION_TARGETS = new Set(['px', 'foreground']) const CUA_REFUSAL_CODE_PATTERN = /^[a-z][a-z0-9_]{0,127}$/ -// Lexical trust-boundary checks for the exact 0.17.0 contract. Callers still treat both handles +// Lexical trust-boundary checks for cua-driver-contract 0.6.0. Callers still treat both handles // as opaque and never derive, increment, or synthesize them. const CUA_SNAPSHOT_ID_PATTERN = /^s[0-9a-f]{8}$/ const CUA_ELEMENT_TOKEN_PATTERN = /^s[0-9a-f]{8}:[0-9]+$/ diff --git a/test/main/plugin/cuaEmbeddedAdapter.test.ts b/test/main/plugin/cuaEmbeddedAdapter.test.ts index ebfed204e..091d0fbf0 100644 --- a/test/main/plugin/cuaEmbeddedAdapter.test.ts +++ b/test/main/plugin/cuaEmbeddedAdapter.test.ts @@ -28,7 +28,7 @@ vi.mock('node:fs', async () => { const contract: CuaEmbeddedRuntimeContract = { hostBundleId: 'com.wefonk.deepchat', - driverVersion: '0.17.0', + driverVersion: '0.19.2', contractVersion: '0.6.0', toolsListSchemaVersion: '1', capabilityVersion: '1', @@ -40,7 +40,7 @@ const cuaEnvironment = { } const metadata = (pid: number): CuaDaemonMetadata => ({ - driver_version: '0.17.0', + driver_version: '0.19.2', contract_version: '0.6.0', tools_list_schema_version: '1', capability_version: '1', diff --git a/test/main/plugin/pluginService.test.ts b/test/main/plugin/pluginService.test.ts index 74cdc58f6..aeb1aec2c 100644 --- a/test/main/plugin/pluginService.test.ts +++ b/test/main/plugin/pluginService.test.ts @@ -1785,13 +1785,13 @@ describe('PluginService', () => { ) expect(manifest.runtime.adapterContract).toEqual({ hostBundleId: 'com.wefonk.deepchat', - driverVersion: '0.17.0', + driverVersion: '0.19.2', contractVersion: '0.6.0', toolsListSchemaVersion: '1', capabilityVersion: '1', mcpProtocolVersion: '2025-06-18' }) - expect(manifest.runtime.install.minVersion).toBe('0.17.0') + expect(manifest.runtime.install.minVersion).toBe('0.19.2') expect(server.args).toEqual(['mcp', '--embedded']) expect(server.env).toBeUndefined() expect(mcpConfig.env).toBeUndefined() @@ -1812,7 +1812,7 @@ describe('PluginService', () => { ) }) - it('keeps CUA v0.17.0 tool policies explicit and conservative', async () => { + it('keeps CUA v0.19.2 tool policies explicit and conservative', async () => { const manifest = JSON.parse(await readFile('plugins/cua/plugin.json', 'utf8')) const policy = JSON.parse(await readFile('plugins/cua/policies/tool-policy.json', 'utf8')) const manifestTools = manifest.toolPolicies.find( @@ -1924,33 +1924,33 @@ describe('PluginService', () => { sourceKind: 'upstream-release', upstreamRepo: 'https://github.com/trycua/cua.git', upstreamSubdir: 'libs/cua-driver/rust', - tag: 'cua-driver-rs-v0.17.0', - commit: '10279552e2bbe479e367a082f78b1b98ee85a697', - version: '0.17.0', - checksumsSha256: 'a3d58c35df49b35c63135a5b969879a7bd831f7d59d9d76b44c6c341ce1bfd7f', + tag: 'cua-driver-rs-v0.19.2', + commit: '20bb34b16ad7c6c56221c332e46b1875e9d8af8c', + version: '0.19.2', + checksumsSha256: '2aa497943793980bba915ebd6ebfab3aae9b7837064464055804862fd03068b4', supportedTargets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64'], unsupportedTargets: ['linux/arm64'] }) expect(metadata.assets).toEqual({ 'darwin-arm64': { - name: 'cua-driver-rs-0.17.0-darwin-arm64.tar.gz', - sha256: '55ed672850492080ff4e7dab4948b4f3bc70c3b84884a89b1eb8521a5d8177a8' + name: 'cua-driver-rs-0.19.2-darwin-arm64.tar.gz', + sha256: 'c30a81f6b5cfd44d40653f7549d7d714b445e9cbd0ed012c4c524f1c43d2872b' }, 'darwin-x64': { - name: 'cua-driver-rs-0.17.0-darwin-x86_64.tar.gz', - sha256: 'cbabf2f71a25b6b158a42384b775d3a75fac15185280d2036d175ee8b49301ae' + name: 'cua-driver-rs-0.19.2-darwin-x86_64.tar.gz', + sha256: 'a9c0ae771f984077dfaddde50f340e52e8b9919f2b8f5917f44334bce92e92de' }, 'windows-x64': { - name: 'cua-driver-rs-0.17.0-windows-x86_64-binary.zip', - sha256: 'f7e366edc4b7148b4f6f78957782b2a2d962620b0daaeb99df7cf9dce6176193' + name: 'cua-driver-rs-0.19.2-windows-x86_64-binary.zip', + sha256: '9868b60999e64ed1028a0f65082624dab7523b06f33b68d582fa0a187d1bf618' }, 'windows-arm64': { - name: 'cua-driver-rs-0.17.0-windows-arm64-binary.zip', - sha256: 'bd3febdabff06331efd0951495f34ef7a5fb2cc230fd5270bd34292bc7ee036a' + name: 'cua-driver-rs-0.19.2-windows-arm64-binary.zip', + sha256: '3976148f985912d03c1876766284898dfbe672db5cc2daa2fb03a0ccb755a1fd' }, 'linux-x64': { - name: 'cua-driver-rs-0.17.0-linux-x86_64-binary.tar.gz', - sha256: 'd8fc3e69fc83f01e6c3a70acef1b16e59a2dfc3c91aa98276360a6d211a61017' + name: 'cua-driver-rs-0.19.2-linux-x86_64-binary.tar.gz', + sha256: '13372e584fcfe4020fe0c5a70ac8d4f0e1c37125f104702d0156d0dddf98fc15' } }) for (const asset of Object.values(metadata.assets) as Array<{ sha256: string }>) { diff --git a/test/main/scripts/buildCuaPluginRuntime.test.ts b/test/main/scripts/buildCuaPluginRuntime.test.ts index c7ffb2a67..08a563cae 100644 --- a/test/main/scripts/buildCuaPluginRuntime.test.ts +++ b/test/main/scripts/buildCuaPluginRuntime.test.ts @@ -199,7 +199,7 @@ describe('build-cua-plugin-runtime', () => { const outputPath = path.join(tempRoot, 'tool-catalog.json') const readCommand = vi.fn(() => JSON.stringify({ - version: '0.17.0', + version: '0.19.2', tools: [ { name: 'click', @@ -220,11 +220,11 @@ describe('build-cua-plugin-runtime', () => { ) await expect( - generateCuaToolCatalog('/runtime/cua-driver', outputPath, '0.17.0', { + generateCuaToolCatalog('/runtime/cua-driver', outputPath, '0.19.2', { readCommand }) ).resolves.toMatchObject({ - version: '0.17.0', + version: '0.19.2', tools: [{ name: 'click' }] }) expect(readCommand).toHaveBeenCalledWith( @@ -235,7 +235,7 @@ describe('build-cua-plugin-runtime', () => { windowsHide: true } ) - await expect(readFile(outputPath, 'utf8')).resolves.toContain('"version": "0.17.0"') + await expect(readFile(outputPath, 'utf8')).resolves.toContain('"version": "0.19.2"') }) it('rejects a generated catalog with a different driver version', async () => { @@ -245,7 +245,7 @@ describe('build-cua-plugin-runtime', () => { generateCuaToolCatalog( '/runtime/cua-driver', path.join(tempRoot, 'tool-catalog.json'), - '0.17.0', + '0.19.2', { readCommand: () => JSON.stringify({ @@ -263,7 +263,7 @@ describe('build-cua-plugin-runtime', () => { }) } ) - ).rejects.toThrow(/Expected 0\.17\.0, got 0\.13\.0/) + ).rejects.toThrow(/Expected 0\.19\.2, got 0\.13\.0/) }) it('rejects malformed safety annotations instead of emitting a partial catalog', async () => { @@ -271,10 +271,10 @@ describe('build-cua-plugin-runtime', () => { const outputPath = path.join(tempRoot, 'tool-catalog.json') await expect( - generateCuaToolCatalog('/runtime/cua-driver', outputPath, '0.17.0', { + generateCuaToolCatalog('/runtime/cua-driver', outputPath, '0.19.2', { readCommand: () => JSON.stringify({ - version: '0.17.0', + version: '0.19.2', tools: [ { name: 'click', diff --git a/test/main/scripts/packagePlugin.test.ts b/test/main/scripts/packagePlugin.test.ts index f0ec5aa38..effc1a8b1 100644 --- a/test/main/scripts/packagePlugin.test.ts +++ b/test/main/scripts/packagePlugin.test.ts @@ -1,12 +1,18 @@ import { spawnSync } from 'node:child_process' -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import fs from 'node:fs' +import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { unzipSync } from 'fflate' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { parseCuaRuntimeIntegrityDescriptor } from '@/plugin/cuaRuntimeIntegrity' +vi.unmock('fs') +vi.unmock('node:fs') +vi.unmock('path') +vi.unmock('node:path') + const ROOT = process.cwd() const tempRoots: string[] = [] const DARWIN_HELPER_APP = 'DeepChat Computer Use.app' @@ -64,7 +70,7 @@ async function createCuaPluginFixture() { integrityDescriptor: 'runtime/${target.platform}/${arch}/integrity.json', adapterContract: { hostBundleId: 'com.wefonk.deepchat', - driverVersion: '0.17.0', + driverVersion: '0.19.2', contractVersion: '0.6.0', toolsListSchemaVersion: '1', capabilityVersion: '1', @@ -101,7 +107,7 @@ async function createCuaPluginFixture() { } const toolCatalog = `${JSON.stringify( { - version: '0.17.0', + version: '0.19.2', tools: [ { name: 'check_permissions', @@ -186,6 +192,18 @@ function runPackagePlugin( ) } +async function loadPackagePlugin() { + return (await import('../../../scripts/package-plugin.mjs')) as { + writeCuaRuntimeIntegrityDescriptor: ( + pluginDir: string, + args: { targetPlatform: string; targetArch: string; purpose?: string } + ) => { + descriptor: Record + descriptorPath: string + } + } +} + describe('package-plugin', () => { afterEach(async () => { await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) @@ -220,7 +238,7 @@ describe('package-plugin', () => { schemaVersion: 1, pluginId: 'com.deepchat.plugins.cua', runtimeId: 'cua-driver', - runtimeVersion: '0.17.0', + runtimeVersion: '0.19.2', target: 'win32/arm64', runtimeRoot: 'runtime/win32/arm64', binaryPath: 'cua-driver.exe', @@ -369,6 +387,78 @@ describe('package-plugin', () => { expect(integrity.files).not.toHaveProperty('integrity.json') }) + it('writes the integrity descriptor required by directory development', async () => { + const fixture = await createCuaPluginFixture() + const { writeCuaRuntimeIntegrityDescriptor } = await loadPackagePlugin() + const integrityPath = path.join( + fixture.pluginDir, + 'runtime', + 'win32', + 'x64', + 'integrity.json' + ) + await writeFile(integrityPath, 'stale descriptor') + + const { descriptor, descriptorPath } = writeCuaRuntimeIntegrityDescriptor( + fixture.pluginDir, + { + targetPlatform: 'win32', + targetArch: 'x64' + } + ) + + expect(descriptorPath).toBe(integrityPath) + expect(JSON.parse(await readFile(descriptorPath, 'utf8'))).toEqual(descriptor) + expect(descriptor).toMatchObject({ + runtimeVersion: '0.19.2', + target: 'win32/x64', + runtimeRoot: 'runtime/win32/x64' + }) + expect((descriptor.files as Record)).not.toHaveProperty('integrity.json') + expect(() => + parseCuaRuntimeIntegrityDescriptor(descriptor, 'directory development fixture') + ).not.toThrow() + }) + + it('restores the previous descriptor when replacement fails', async () => { + const fixture = await createCuaPluginFixture() + const { writeCuaRuntimeIntegrityDescriptor } = await loadPackagePlugin() + const runtimeDir = path.join(fixture.pluginDir, 'runtime', 'win32', 'x64') + const integrityPath = path.join(runtimeDir, 'integrity.json') + const previousDescriptor = 'previous descriptor' + await writeFile(integrityPath, previousDescriptor) + + const renameSync = fs.renameSync.bind(fs) + let replacementAttempts = 0 + const renameSpy = vi.spyOn(fs, 'renameSync').mockImplementation((source, destination) => { + if (String(source).endsWith('.tmp') && String(destination) === integrityPath) { + replacementAttempts += 1 + throw Object.assign( + new Error(replacementAttempts === 1 ? 'target exists' : 'replacement failed'), + { code: replacementAttempts === 1 ? 'EEXIST' : 'EIO' } + ) + } + renameSync(source, destination) + }) + + try { + expect(() => + writeCuaRuntimeIntegrityDescriptor(fixture.pluginDir, { + targetPlatform: 'win32', + targetArch: 'x64' + }) + ).toThrow('replacement failed') + } finally { + renameSpy.mockRestore() + } + + expect(replacementAttempts).toBe(2) + expect(await readFile(integrityPath, 'utf8')).toBe(previousDescriptor) + expect( + (await readdir(runtimeDir)).filter((name) => name.startsWith('.integrity.json-')) + ).toEqual([]) + }) + it('records the explicit macOS distribution identity in the integrity descriptor', async () => { const fixture = await createCuaPluginFixture() const outDir = path.join(fixture.root, 'out')