diff --git a/e2e/appduct-fixtures.test.ts b/e2e/appduct-fixtures.test.ts new file mode 100644 index 00000000..d9a99412 --- /dev/null +++ b/e2e/appduct-fixtures.test.ts @@ -0,0 +1,109 @@ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const FIXTURES = "e2e/fixtures/appduct"; +const README = `${FIXTURES}/README.md`; + +interface Tracked { + /** why: path relative to `e2e/fixtures/appduct/`. */ + path: string; + mode: string; +} + +function trackedFixtures(): Tracked[] { + return execFileSync("git", ["ls-files", "-s", "-z", FIXTURES], { + cwd: REPO_ROOT, + encoding: "buffer", + }) + .toString("utf8") + .split("\0") + .filter((entry) => entry.length > 0) + .map((entry) => { + const [meta = "", path = ""] = entry.split("\t"); + return { mode: meta.split(" ")[0] ?? "", path: path.slice(FIXTURES.length + 1) }; + }); +} + +function readme(): string { + return readFileSync(join(REPO_ROOT, README), "utf8"); +} + +/** why: the rows of the only table under `## Files`: path and SHA-256, cells trimmed, backticks removed. */ +function readmeTable(): Array<{ path: string; sha256: string }> { + const section = readme().split(/^## Files\s*$/m)[1] ?? ""; + const body = section.split(/^## /m)[0] ?? ""; + return body + .split("\n") + .filter((line) => line.trim().startsWith("|")) + .map((line) => + line + .split("|") + .slice(1, -1) + .map((cell) => cell.trim()), + ) + .filter((cells) => cells.length === 2 && !/^-+$/.test(cells[0] ?? "") && cells[0] !== "File") + .map(([path = "", sha256 = ""]) => ({ + path: path.replace(/^`|`$/g, ""), + sha256: sha256.replace(/^`|`$/g, ""), + })); +} + +describe("Appduct fixture apps", () => { + it("lists every tracked file under e2e/fixtures/appduct/ except the README in the README's table, with the SHA-256 of the file on disk", () => { + const files = trackedFixtures() + .map((file) => file.path) + .filter((path) => path !== "README.md"); + const table = readmeTable(); + + expect(files.length).toBeGreaterThan(0); + expect(table.map((row) => row.path).sort()).toEqual(expect.arrayContaining(files.sort())); + for (const row of table.filter((entry) => files.includes(entry.path))) { + expect(row.sha256).toMatch(/^[0-9a-f]{64}$/); + const onDisk = createHash("sha256") + .update(readFileSync(join(REPO_ROOT, FIXTURES, row.path))) + .digest("hex"); + expect(onDisk, row.path).toBe(row.sha256); + } + }); + + it("tracks ios/AppductPlayground.app/Info.plist, ios/AppductPlayground.app/AppductPlayground and android/AppductPlayground.apk under exactly those names", () => { + const paths = trackedFixtures().map((file) => file.path); + + expect(paths).toEqual( + expect.arrayContaining([ + "ios/AppductPlayground.app/Info.plist", + "ios/AppductPlayground.app/AppductPlayground", + "android/AppductPlayground.apk", + ]), + ); + }); + + it("lists no file in the README's table that git does not track under e2e/fixtures/appduct/", () => { + const tracked = trackedFixtures().map((file) => file.path); + const table = readmeTable(); + + expect(table.length).toBeGreaterThan(0); + expect(table.map((row) => row.path).filter((path) => !tracked.includes(path))).toEqual([]); + }); + + it("tracks the iOS executable ios/AppductPlayground.app/AppductPlayground with mode 100755", () => { + const executable = trackedFixtures().find( + (file) => file.path === "ios/AppductPlayground.app/AppductPlayground", + ); + + expect(executable?.mode).toBe("100755"); + }); + + it("has a Repo: line naming https://github.com/callstackincubator/appduct and a Commit: line with 40 lowercase hex digits", () => { + const text = readme(); + + expect(text).toMatch(/^Repo: https:\/\/github\.com\/callstackincubator\/appduct$/m); + expect(text).toMatch(/^Commit: `[0-9a-f]{40}`$/m); + }); +}); diff --git a/e2e/fixtures/appduct/README.md b/e2e/fixtures/appduct/README.md new file mode 100644 index 00000000..ddda273e --- /dev/null +++ b/e2e/fixtures/appduct/README.md @@ -0,0 +1,75 @@ +# Appduct playground fixture apps + +Two built apps for the agent-device slow tests (#477): an iOS simulator build +and an Android debug build of Appduct's native playground. They are committed +so a test installs them without building an app or downloading anything. + +Repo: https://github.com/callstackincubator/appduct +Commit: `0e68131bdd4b8a789314812caf1995b496c52ae9` + +The commit is the tag `v0.15.0`. Both apps are built from it. + +## Build commands + +```bash +# iOS, from playground-native/ios at the pin (needs Xcode and xcodegen) +xcodegen generate +xcodebuild build -project AppductPlayground.xcodeproj -scheme AppductPlayground \ + -configuration Debug -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' -derivedDataPath build \ + ARCHS=arm64 CODE_SIGNING_ALLOWED=NO +# -> build/Build/Products/Debug-iphonesimulator/AppductPlayground.app + +# Android, from playground-native/android at the pin (needs JDK 17 and ANDROID_HOME) +./gradlew :app:assembleDebug +# -> app/build/outputs/apk/debug/app-debug.apk, committed as AppductPlayground.apk +``` + +Where each output goes: + +- `AppductPlayground.app` is copied whole to `ios/AppductPlayground.app/`. +- `app-debug.apk` is copied to `android/AppductPlayground.apk`. + +## Tool versions of this build + +- Xcode 27.0 (build 27A266a) +- xcodegen 2.45.4 +- JDK 17 (Zulu), Gradle 8.14.3 (the wrapper's) +- Android SDK platform 35 (`compileSdk`) + +## App ids + +- iOS: `com.callstack.appduct.playgroundnative` +- Android: `com.callstack.appduct.playground` + +## Screen ids + +From Appduct's playground contract; #477 uses these: `tab-tools`, +`tab-status`, `connection-state`, `call-count`. + +## Notes + +- Both are debug builds, as Appduct's own device suite uses. +- The iOS build runs on arm64 simulators only (Apple silicon Macs). It is + unsigned; the arm64 linker signs it ad hoc, which the simulator accepts. +- The Android build is signed with the builder's debug key. + +## Files + +| File | SHA-256 | +| --------------------------------------------------------- | ------------------------------------------------------------------ | +| `android/AppductPlayground.apk` | `023e23a7f47f53c0b7914088b6f91d802212297faad126dadf1de81982d9f2a7` | +| `ios/AppductPlayground.app/AppductPlayground` | `1d434163aab86e0266796e6fada8619c296193e086f84ddd1495290c4ba27fea` | +| `ios/AppductPlayground.app/AppductPlayground.debug.dylib` | `d4e2ef2949c1a1b27ee70bc79b23b40dd8b470bf98047372503f6c9dc1ee38ca` | +| `ios/AppductPlayground.app/Info.plist` | `444c14b8188fd231c52e4e3711ca681ec370ad1883d145e4ef6533c05e52dc90` | +| `ios/AppductPlayground.app/PkgInfo` | `82502191c9484b04d685374f9879a0066069c49b8acae7a04b01d38d07e8eca0` | +| `ios/AppductPlayground.app/__preview.dylib` | `2b569eb3932de0d2908fa99589f9078d6d9abc5bea8c9fbf03d84980986dd287` | + +## Refresh + +1. Build at the new pin with the commands above. +2. Replace the files under `ios/` and `android/`. +3. Update the tag, the commit, the tool versions and the table above in the + same commit. +4. Check by hand: `plutil -extract CFBundleIdentifier raw` on `Info.plist`, + `lipo -info` on the executable, and `aapt dump badging` on the APK. diff --git a/e2e/fixtures/appduct/android/AppductPlayground.apk b/e2e/fixtures/appduct/android/AppductPlayground.apk new file mode 100644 index 00000000..4e098056 Binary files /dev/null and b/e2e/fixtures/appduct/android/AppductPlayground.apk differ diff --git a/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground b/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground new file mode 100755 index 00000000..60f9a2ab Binary files /dev/null and b/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground differ diff --git a/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground.debug.dylib b/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground.debug.dylib new file mode 100755 index 00000000..cf82a099 Binary files /dev/null and b/e2e/fixtures/appduct/ios/AppductPlayground.app/AppductPlayground.debug.dylib differ diff --git a/e2e/fixtures/appduct/ios/AppductPlayground.app/Info.plist b/e2e/fixtures/appduct/ios/AppductPlayground.app/Info.plist new file mode 100644 index 00000000..2ee598de Binary files /dev/null and b/e2e/fixtures/appduct/ios/AppductPlayground.app/Info.plist differ diff --git a/e2e/fixtures/appduct/ios/AppductPlayground.app/PkgInfo b/e2e/fixtures/appduct/ios/AppductPlayground.app/PkgInfo new file mode 100644 index 00000000..bd04210f --- /dev/null +++ b/e2e/fixtures/appduct/ios/AppductPlayground.app/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/e2e/fixtures/appduct/ios/AppductPlayground.app/__preview.dylib b/e2e/fixtures/appduct/ios/AppductPlayground.app/__preview.dylib new file mode 100755 index 00000000..b424db1b Binary files /dev/null and b/e2e/fixtures/appduct/ios/AppductPlayground.app/__preview.dylib differ diff --git a/e2e/physical-leasing.test.ts b/e2e/physical-leasing.test.ts index d4c3aadc..e238ca99 100644 --- a/e2e/physical-leasing.test.ts +++ b/e2e/physical-leasing.test.ts @@ -510,9 +510,9 @@ describe("a physical lease request that waits", () => { }, ); const first = await granted(env, physical("agent-a")); - await release(env, first); const waiter = env.cliBackground(physical("agent-b")); await queuedAt(waiter, 1); + await release(env, first); const ended = await waiter.waitForExit(30_000); diff --git a/src/source-hygiene.test.ts b/src/source-hygiene.test.ts index d41e8f9e..c5310fc5 100644 --- a/src/source-hygiene.test.ts +++ b/src/source-hygiene.test.ts @@ -10,14 +10,15 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NUL = 0x00; /** - * A raw NUL byte in a text file makes `grep` and `ripgrep` classify the whole file as + * why: a raw NUL byte in a text file makes `grep` and `ripgrep` classify the whole file as * binary and print `binary file matches` instead of the matching lines -- so the file * stops being searchable for every human and agent working in this repo. The byte is * almost always a `"\0"` escape that was written as the literal character instead. * * Enumerated from `git ls-files` rather than a directory walk so the check sees every * tracked file in the repository (testing rule 4), not just the directories this test - * happened to name. Untracked and ignored files are out of scope: they are not part of + * happened to name. The one exception is `e2e/fixtures/`: built binaries committed for tests, + * never searched as text, so they are skipped. Untracked and ignored files are out of scope: they are not part of * what anyone clones or searches. */ describe("tracked files", () => { @@ -29,6 +30,7 @@ describe("tracked files", () => { .toString("utf8") .split("\0") .filter((name) => name.length > 0) + .filter((name) => !name.startsWith("e2e/fixtures/")) // A submodule or a path deleted from the working tree is still listed by // `git ls-files`; only regular files can be read. .filter((name) => { @@ -45,7 +47,12 @@ describe("tracked files", () => { expect(trackedFiles.length).toBeGreaterThan(100); }); - it("contain no raw NUL byte, so grep and ripgrep can search them", () => { + it("hold no path under e2e/fixtures/ and still hold e2e/appduct-fixtures.test.ts", () => { + expect(trackedFiles.filter((name) => name.startsWith("e2e/fixtures/"))).toEqual([]); + expect(trackedFiles).toContain("e2e/appduct-fixtures.test.ts"); + }); + + it("outside e2e/fixtures/ contain no raw NUL byte, so grep and ripgrep can search them", () => { const offenders = trackedFiles .map((name) => ({ name, contents: readFileSync(join(REPO_ROOT, name)) })) .filter(({ contents }) => contents.includes(NUL))