Files
vphone-cli/tests/VPhoneCoreTests/BundleOpsTests.swift
T
zqxwceandClaude Opus 4.8 828be6dbc6 feat: add VPhoneCore library — VM bundle model + host primitives
The library the consolidated CLI is built on:
- bundle/library model (VPhoneBundle, VPhoneLibrary, VPhoneVirtualMachineManifest),
  bundle ops + reporting, restore helpers (VPhoneRestoreOps)
- host process primitives: VPhoneProcessRunner, VPhoneManagedProcess (spawn +
  stdout pattern-match + SIGKILL-escalating terminate), VPhoneLaunchLayout,
  VPhoneBootPatterns
- VPhoneResources: bundled-.app vs dev asset resolution, and Python resolution
  that provisions a per-user venv (~/.vphone/venv) on demand so the app is
  portable — never depends on the repo's .venv
- VPhoneVerbosity (quiet/info/debug/trace) and VPhoneVMPicker
Manifest moves out of the executable target into VPhoneCore. Full unit-test
suite under tests/VPhoneCoreTests.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
2026-07-27 19:46:27 +03:00

326 lines
17 KiB
Swift

@testable import VPhoneCore
import Foundation
import Testing
struct BundleOpsTests {
private func makeRoot() throws -> URL {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
private func fakeROM() throws -> URL {
let f = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".bin")
try Data([0xAA, 0xBB, 0xCC]).write(to: f)
return f
}
@Test func createsBundleWithSparseDiskAndManifest() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let spec = VPhoneBundleOps.NewBundleSpec(
name: "newvm", cpuCount: 8, memoryMB: 8192, diskSizeGB: 64,
romSource: rom, sepromSource: seprom)
let bundle = try VPhoneBundleOps.create(spec, in: VPhoneLibrary(root: root))
#expect(bundle.manifest.cpuCount == 8)
#expect(bundle.manifest.memorySize == 8192 * 1024 * 1024)
let disk = bundle.url.appendingPathComponent("Disk.img")
let size = (try FileManager.default.attributesOfItem(atPath: disk.path)[.size] as? NSNumber)?.int64Value
#expect(size == Int64(64 * 1024 * 1024 * 1024))
#expect(FileManager.default.fileExists(atPath: bundle.url.appendingPathComponent("SEPStorage").path))
#expect(FileManager.default.fileExists(atPath: bundle.url.appendingPathComponent("AVPBooter.vresearch1.bin").path))
}
@Test func rejectsDuplicateName() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let spec = VPhoneBundleOps.NewBundleSpec(
name: "dup", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom)
_ = try VPhoneBundleOps.create(spec, in: VPhoneLibrary(root: root))
#expect(throws: VPhoneLibraryError.self) {
_ = try VPhoneBundleOps.create(spec, in: VPhoneLibrary(root: root))
}
}
@Test func rejectsInvalidNames() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
for bad in ["", "a/b", ".hidden"] {
#expect(throws: VPhoneLibraryError.self) {
_ = try VPhoneBundleOps.create(
.init(name: bad, cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
}
}
}
@Test func rollsBackPartialBundleOnFailure() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let lib = VPhoneLibrary(root: root)
// A non-existent ROM source makes copyItem fail AFTER the dir is created.
let missingRom = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString + ".bin")
#expect(throws: (any Error).self) {
_ = try VPhoneBundleOps.create(
.init(name: "partial", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: missingRom, sepromSource: missingRom), in: lib)
}
// The half-built directory must be removed so the name is reusable.
#expect(!FileManager.default.fileExists(atPath: lib.url(forName: "partial").path))
}
@Test func updateConfigPersistsFields() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
_ = try VPhoneBundleOps.create(
.init(name: "cfg", cpuCount: 8, memoryMB: 8192, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let updated = try VPhoneBundleOps.updateConfig(
bundleNamed: "cfg", in: lib, cpuCount: 4, memoryMB: nil)
#expect(updated.manifest.cpuCount == 4)
#expect(updated.manifest.memorySize == 8192 * 1024 * 1024)
// Persisted: a fresh load sees the change.
#expect(try lib.bundle(named: "cfg").manifest.cpuCount == 4)
}
@Test func renameThenDelete() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
_ = try VPhoneBundleOps.create(
.init(name: "old", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let renamed = try VPhoneBundleOps.rename(bundleNamed: "old", to: "shiny", in: lib)
#expect(renamed.name == "shiny")
#expect(throws: VPhoneLibraryError.self) { _ = try lib.bundle(named: "old") }
try VPhoneBundleOps.delete(bundleNamed: "shiny", in: lib)
#expect(try lib.bundles().isEmpty)
}
@Test func renameRejectsExistingTarget() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
for n in ["a", "b"] {
_ = try VPhoneBundleOps.create(
.init(name: n, cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
}
#expect(throws: VPhoneLibraryError.alreadyExists(name: "b")) {
_ = try VPhoneBundleOps.rename(bundleNamed: "a", to: "b", in: lib)
}
}
@Test func cloneCopiesBundleAndResetsIdentity() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
let src = try VPhoneBundleOps.create(
.init(name: "src", cpuCount: 8, memoryMB: 4096, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
// Simulate a booted/restored VM: identity artifacts present + non-empty machineIdentifier.
let fm = FileManager.default
try Data([1, 2, 3]).write(to: src.url.appendingPathComponent("nvram.bin"))
try Data([4]).write(to: src.url.appendingPathComponent("udid-prediction.txt"))
try Data([5]).write(to: src.url.appendingPathComponent("ABC123.shsh"))
let withID = src.manifest.updating(machineIdentifier: Data([9, 9]))
try withID.write(to: src.configURL)
let clone = try VPhoneBundleOps.clone(bundleNamed: "src", to: "dst", in: lib)
// Copy happened (disk + ROMs present in the clone).
#expect(fm.fileExists(atPath: clone.url.appendingPathComponent("Disk.img").path))
#expect(fm.fileExists(atPath: clone.url.appendingPathComponent("AVPBooter.vresearch1.bin").path))
// Identity artifacts cleared in the clone.
#expect(!fm.fileExists(atPath: clone.url.appendingPathComponent("nvram.bin").path))
#expect(!fm.fileExists(atPath: clone.url.appendingPathComponent("udid-prediction.txt").path))
#expect(!fm.fileExists(atPath: clone.url.appendingPathComponent("ABC123.shsh").path))
#expect(clone.manifest.machineIdentifier.isEmpty)
// Original untouched.
#expect(fm.fileExists(atPath: src.url.appendingPathComponent("nvram.bin").path))
#expect(try lib.bundle(named: "src").manifest.machineIdentifier == Data([9, 9]))
}
@Test func cloneRejectsExistingTarget() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
for n in ["a", "b"] {
_ = try VPhoneBundleOps.create(
.init(name: n, cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
}
#expect(throws: VPhoneLibraryError.alreadyExists(name: "b")) {
_ = try VPhoneBundleOps.clone(bundleNamed: "a", to: "b", in: lib)
}
}
@Test func exportThenImportRoundTrips() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
_ = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 8, memoryMB: 4096, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let archive = root.appendingPathComponent("orig.tgz")
try VPhoneBundleOps.export(bundleNamed: "orig", to: archive, includeIPSW: false, in: lib)
#expect(FileManager.default.fileExists(atPath: archive.path))
// Import into a fresh library, renaming.
let root2 = try makeRoot()
defer { try? FileManager.default.removeItem(at: root2) }
let lib2 = VPhoneLibrary(root: root2)
let imported = try VPhoneBundleOps.importArchive(from: archive, name: "copy", in: lib2)
#expect(imported.name == "copy")
#expect(imported.manifest.cpuCount == 8)
#expect(imported.manifest.memorySize == 4096 * 1024 * 1024)
#expect(try lib2.bundle(named: "copy").manifest.cpuCount == 8)
}
@Test func importRejectsExistingName() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
_ = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let archive = root.appendingPathComponent("orig.tgz")
try VPhoneBundleOps.export(bundleNamed: "orig", to: archive, includeIPSW: false, in: lib)
// Importing back under the same existing name must fail.
#expect(throws: VPhoneLibraryError.alreadyExists(name: "orig")) {
_ = try VPhoneBundleOps.importArchive(from: archive, name: nil, in: lib)
}
}
@Test func importWithRenameDoesNotClobberArchivedNameCollision() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
// Existing "orig" (cpu 16) that must NOT be touched by the import.
_ = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 16, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
// Archive of a DIFFERENT "orig" (cpu 4) from a separate library.
let root2 = try makeRoot()
defer { try? FileManager.default.removeItem(at: root2) }
let lib2 = VPhoneLibrary(root: root2)
_ = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 4, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib2)
let archive = root2.appendingPathComponent("orig.tgz")
try VPhoneBundleOps.export(bundleNamed: "orig", to: archive, includeIPSW: false, in: lib2)
let imported = try VPhoneBundleOps.importArchive(from: archive, name: "renamed", in: lib)
#expect(imported.name == "renamed")
#expect(imported.manifest.cpuCount == 4)
#expect(try lib.bundle(named: "orig").manifest.cpuCount == 16) // untouched
}
@Test func exportExcludesRestoreDirByDefault() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
let b = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let restoreDir = b.url.appendingPathComponent("iPhone_Restore")
try FileManager.default.createDirectory(at: restoreDir, withIntermediateDirectories: true)
try Data([0]).write(to: restoreDir.appendingPathComponent("marker"))
let archive = root.appendingPathComponent("orig.tgz")
try VPhoneBundleOps.export(bundleNamed: "orig", to: archive, includeIPSW: false, in: lib)
let listing = try VPhoneProcessRunner.runCapturing(
URL(fileURLWithPath: "/usr/bin/tar"), ["-tf", archive.path])
#expect(listing.succeeded)
#expect(!listing.stdout.contains("iPhone_Restore"))
}
@Test func exportExcludesRegenerableStagingFiles() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
let rom = try fakeROM(); let seprom = try fakeROM()
defer { try? FileManager.default.removeItem(at: rom); try? FileManager.default.removeItem(at: seprom) }
let lib = VPhoneLibrary(root: root)
let b = try VPhoneBundleOps.create(
.init(name: "orig", cpuCount: 2, memoryMB: 2048, diskSizeGB: 1,
romSource: rom, sepromSource: seprom), in: lib)
let fm = FileManager.default
try Data([0]).write(to: b.url.appendingPathComponent(".vphoned.signed"))
try fm.createDirectory(at: b.url.appendingPathComponent("cfw_input/jb"), withIntermediateDirectories: true)
try Data([0]).write(to: b.url.appendingPathComponent("cfw_input/jb/f"))
try fm.createDirectory(at: b.url.appendingPathComponent("cfw_jb_input"), withIntermediateDirectories: true)
try Data([0]).write(to: b.url.appendingPathComponent("cfw_jb_input/f"))
try fm.createDirectory(at: b.url.appendingPathComponent(".cfw_temp"), withIntermediateDirectories: true)
try Data([0]).write(to: b.url.appendingPathComponent(".cfw_temp/f"))
let archive = root.appendingPathComponent("orig.tgz")
try VPhoneBundleOps.export(bundleNamed: "orig", to: archive, includeIPSW: false, in: lib)
let listing = try VPhoneProcessRunner.runCapturing(
URL(fileURLWithPath: "/usr/bin/tar"), ["-tf", archive.path])
#expect(listing.succeeded)
#expect(!listing.stdout.contains(".vphoned.signed"))
#expect(!listing.stdout.contains("cfw_input"))
#expect(!listing.stdout.contains("cfw_jb_input"))
#expect(!listing.stdout.contains(".cfw_temp"))
// The real payload still travels.
#expect(listing.stdout.contains("Disk.img"))
#expect(listing.stdout.contains("config.plist"))
}
@Test func importRejectsMultiTopLevelArchive() throws {
let root = try makeRoot()
defer { try? FileManager.default.removeItem(at: root) }
// An archive with TWO top-level dirs is not a single bundle → badArchive.
let src = root.appendingPathComponent("src")
try FileManager.default.createDirectory(
at: src.appendingPathComponent("a"), withIntermediateDirectories: true)
try FileManager.default.createDirectory(
at: src.appendingPathComponent("b"), withIntermediateDirectories: true)
try Data([0]).write(to: src.appendingPathComponent("a/x"))
try Data([0]).write(to: src.appendingPathComponent("b/y"))
let archive = root.appendingPathComponent("multi.tgz")
let made = try VPhoneProcessRunner.runCapturing(
URL(fileURLWithPath: "/usr/bin/tar"), ["-czf", archive.path, "-C", src.path, "a", "b"])
#expect(made.succeeded)
#expect(throws: VPhoneBundleOpsError.self) {
_ = try VPhoneBundleOps.importArchive(from: archive, name: nil, in: VPhoneLibrary(root: root))
}
}
}