mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 08:54:04 +01:00
* fix(ios-qa): allow physical iPad devices in tunnel bootstrap (#2743)
(cherry picked from commit d6d43934ef)
* fix(ios-qa): iPhone + iPad on USB names each UDID instead of picking one
With GSTACK_IOS_TARGET_UDID unset, tied default ranks used to pick the first
device devicectl listed, so a QA session could drive the wrong device. Tied
candidates now fail with multiple_devices, list each device and UDID, and
print a ready export line. Device selection moves into selectDevice() so the
route-drop recovery can re-resolve with the same rules.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ios-qa): a dropped CoreDevice route no longer relaunches the app (#1975)
Rotation deletes the app's one-shot boot-token file, so the daemon's old
503/504 path (invalidate + full bootstrap) could only get a new token by
running devicectl launch --terminate-existing, wiping the in-app QA state at
every route blip.
A route failure now goes through recoverTunnel(): re-select the device with
the bootstrap rules, re-resolve the tunnel address for the pinned UDID, check
the unauthenticated /healthz owner, then probe /state/snapshot with the cached
bearer. Outcomes:
- bearer accepted: keep the session (new address if it moved)
- 401: full bootstrap from the fresh boot token (app was relaunched)
- app confirmed absent: bootstrap, which launches it normally once
- different device or an address devicectl cannot tie to the UDID:
bootstrap; the old bearer is never sent there
- route still down with the app running: surface the error, keep the session
Concurrent failures on one tunnel share a single refresh. Mutations whose
response was lost are still never replayed. The CLI wiring moves into
deviceTunnelSource() so tests drive it against a simulated device.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ios-qa): a failed boot-token write is loud instead of silent (#1837)
StateServer.start() wrote the boot-token file with try?, so a failed write
left nothing on disk and no trace anywhere, and the daemon then relaunched the
app trying to mint a token that could never be written. The write is now a
do/catch that logs the path and error (never the token), logs a NOT READY
bootstrap line, and reports boot_token_error on /healthz. bootstrapTunnel
reads that field and returns boot_token_unavailable naming the device-side
cause instead of running --terminate-existing.
Also drops the stale comments claiming the token was written to os_log.
Template and fixture stay byte-identical.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ios-qa): type the iPad acceptance test's mDNS stub
ResolveImpl returns Promise<string[]>; resolving null tripped the test-code
typecheck ratchet. A rejected lookup exercises the same resolve_failed path.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(ios-qa): iPad support, GSTACK_IOS_TARGET_UDID, route-drop recovery, known limits
- iPhone-only description, triggers, and prerequisites now cover iPads, and
say how to pick one device with GSTACK_IOS_TARGET_UDID.
- Phase 2 no longer tells you to capture a boot token from os_log (it has
not been logged for many releases); it describes the 0600 tmp/ file and
the NOT READY signal (#1837, #1735).
- Source-control guidance for the generated DebugBridge/ package (#1735).
- Failure table: route drops keep the session, multiple_devices, unwritable
token file, and the one relaunch a daemon restart still costs.
- Known limits: SwiftUI DragGesture and /swipe on iOS 26 (#1975), the
iOS 26.3.1 /elements data from PR #1755, unverified iPad Stage Manager.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: iOS QA how-to and skill index cover iPads and route-drop recovery
The how-to still said the boot token was readable from os_log and that a
route drop re-bootstraps; both are now accurate, with iPad selection,
GSTACK_IOS_TARGET_UDID, and three new failure rows.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.37.0 fix(ios-qa): iPads work, a route drop no longer relaunches the app, loud boot-token write
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(ios-qa): keep issue numbers out of model-read skill text
The archaeology lint rejects issue references in generated SKILL.md text;
the Known limits entry keeps the iOS version and drops the number.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: re-slot to v1.91.38.0 (v1.91.37.0 claimed by #3075)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ios-qa): device-change recovery test without an IPv4-mapped address
The Windows lane could not reach a fake StateServer through
::ffff:127.0.0.1, so the iPad half of the device-change test timed out. The
fake device now swaps identity on its one ::1 listener, which is also closer
to production: every StateServer answers on the same port.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: loulanyue <260355617@qq.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
/**
|
|
* StateServer hardening pins (fork port wave 2, B3 + B5).
|
|
*
|
|
* B3: the boot token must never appear in an os_log statement. The daemon
|
|
* reads it from the 0600 app-container file (copyFileFromAppContainer in
|
|
* tunnel-bootstrap.ts); the old `token=\(self.bootToken, privacy: .public)`
|
|
* announce line had NO consumer and handed a live credential to anything
|
|
* reading the unified log during the launch window.
|
|
*
|
|
* B5: the IPv4 listener has no CoreDevice tunnel path, so it must bind to
|
|
* loopback at the socket level (requiredLocalEndpoint 127.0.0.1), not rely
|
|
* solely on the per-connection peer check. IPv6 keeps the wildcard bind for
|
|
* CoreDevice ULA peers by design.
|
|
*
|
|
* #1837: the boot-token file is the daemon's only way in, so writing it must
|
|
* not be a silent `try?`. A failure is logged and reported on /healthz (the
|
|
* daemon then names the device-side cause instead of relaunching the app).
|
|
*
|
|
* Pinned on BOTH the generated template and the fixture app copy so neither
|
|
* can drift back independently.
|
|
*/
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import { readFileSync } from "fs";
|
|
import { join } from "path";
|
|
|
|
const ROOT = join(import.meta.dir, "..");
|
|
const COPIES = [
|
|
"ios-qa/templates/StateServer.swift.template",
|
|
"test/fixtures/ios-qa/FixtureApp/Sources/DebugBridgeCore/StateServer.swift",
|
|
];
|
|
|
|
describe.each(COPIES)("StateServer hardening — %s", (rel) => {
|
|
const src = readFileSync(join(ROOT, rel), "utf-8");
|
|
|
|
test("no os_log statement interpolates the boot token (B3)", () => {
|
|
const logLines = src.split("\n").filter((l) => /logger\.(notice|info|error|debug|log)/.test(l));
|
|
for (const line of logLines) {
|
|
expect(line).not.toContain("bootToken");
|
|
}
|
|
// The bootstrap announce survives (diagnostics), token-free.
|
|
expect(src).toContain('gstack-ios-qa-bootstrap port=');
|
|
expect(src).not.toContain("gstack-ios-qa-bootstrap token=");
|
|
});
|
|
|
|
test("boot-token write failures are loud, not a silent try? (#1837)", () => {
|
|
expect(src).not.toMatch(/try\?\s*bootToken\.write/);
|
|
expect(src).not.toMatch(/try\?\s*FileManager\.default\.setAttributes\(\[\.posixPermissions: 0o600\]/);
|
|
const start = src.slice(src.indexOf("public func start()"), src.indexOf("public func register("));
|
|
expect(start).toMatch(/do \{\s*try bootToken\.write\(toFile: tokenFile/);
|
|
expect(start).toMatch(/\} catch \{\s*bootTokenWriteError = error\.localizedDescription\s*logger\.error\(/);
|
|
expect(start).toContain("gstack-ios-qa-bootstrap NOT READY");
|
|
expect(src).toContain('health["boot_token_error"] = failure');
|
|
});
|
|
|
|
test("comments no longer claim os_log carries the boot token", () => {
|
|
expect(src).not.toMatch(/os_log scrape|wrote to os_log|scraping\s*\n?\/\/\s*os_log/);
|
|
});
|
|
|
|
test("IPv4 listener binds loopback at the socket level (B5)", () => {
|
|
expect(src).toContain("requiredLocalEndpoint");
|
|
expect(src).toContain('NWEndpoint.Host("127.0.0.1")');
|
|
// IPv6 wildcard + peer-check path must survive (CoreDevice tunnel peers).
|
|
expect(src).toMatch(/case \.ipv6:\s*\n\s*listener = try NWListener\(using: params, on:/);
|
|
});
|
|
});
|