Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ jobs:
- uses: actions/checkout@v4
- name: Verify Apple app icon packaging
run: python3 .agents/skills/journal-build/scripts/test_apple_app_icon.py
- name: Verify Swift runtime startup and failure boundaries
- name: Verify Swift runtime and graph feedback boundaries
run: |
python3 tool/test_journal_runtime_startup.py
python3 tool/test_journal_runtime_failure.py
python3 tool/test_graph_name.py
- name: Prepare native system libraries before cache lookup
run: |
brew install pkg-config gmp sqlite zlib zstd
Expand Down
21 changes: 21 additions & 0 deletions app/application.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6851,6 +6851,26 @@ let start
let owners=Signal.state context.Lui_ui.ui_scheduler [] in
let payload=Signal.state context.ui_scheduler "{}" in
let refreshers=Hashtbl.create 3 in
(* Resolve the at-most-three resident names only when the catalog or resident
set changes; typing and worker refreshes must not scan the full catalog. *)
let name_catalog = ref None and name_residents = ref [] in
let resident_names = Hashtbl.create 3 in
let target_graph_name state =
match state.manager, next_resident_owner state with
| Some manager, Some owner ->
let same_catalog = match !name_catalog with
| Some previous -> previous == state.graph_catalog | None -> false in
if not same_catalog || !name_residents <> manager.resident_graphs then (
name_catalog := Some state.graph_catalog;
name_residents := manager.resident_graphs;
Hashtbl.clear resident_names;
Rrbvec.to_seq state.graph_catalog |> Seq.iter (fun (graph:Graph_service.graph) ->
if List.mem graph.graph_id manager.resident_graphs then
Hashtbl.replace resident_names graph.graph_id graph.name));
(match Hashtbl.find_opt resident_names owner.graph_id with
| Some name -> `String name | None -> `Null)
| _ -> name_catalog := None; name_residents := []; Hashtbl.clear resident_names; `Null
in
let root ()=Signal.sample model_signal in
let host_model state owner =
let model=graph_model state owner in
Expand Down Expand Up @@ -6883,6 +6903,7 @@ let start
;"pageID",(match selected with Some id->`String id | None->`Null)
;"targetID",(match state.graph_preview with Some id->`String id | None->`Null)
;"targetPageID",(match next_resident_owner state with Some owner->`String(owner_key owner) | None->`Null)
;"targetGraphName",target_graph_name state
;"residentPageIDs",`List(List.map(fun owner->`String(owner_key owner))live)
;"slotOwners",`List(List.map(fun owner->`String(owner_key owner))next)
;"loadingPageIDs",`List(List.filter_map(fun owner->let model=host_model state owner in
Expand Down
117 changes: 117 additions & 0 deletions apple-tests/native-list/JournalGraphNameAcceptance.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import XCTest

/// The native gesture/overlay lifetime is not observable at the OCaml reducer.
/// Run against an isolated three-resident offline warm-start fixture, not an account.
final class JournalGraphNameAcceptance: XCTestCase {
@MainActor private func host(dark: Bool = false, longName: Bool = false, accessibility: Bool = false) -> XCUIApplication {
continueAfterFailure = false
let env = ProcessInfo.processInfo.environment
let app = XCUIApplication(bundleIdentifier: env["JOURNAL_GRAPH_NAME_APP"] ?? "org.logseq.journal.graph-name-probe")
app.launchEnvironment = ["LOGSEQ_JOURNAL_E2EE_TEST_PRIVATE_KEY_STORAGE": "memory",
"LOGSEQ_JOURNAL_E2EE_TEST_WRAPPED_KEY_STORAGE": "memory"]
app.launchArguments = ["--fixture", longName ? "long.json" : "valid.json", "--support-root", longName ? "support-long" : "support-valid", "--application-only",
dark ? "--dark-appearance" : "--light-appearance"]
if accessibility { app.launchArguments.append("--accessibility-size") }
app.launch()
XCTAssertTrue(app.collectionViews.firstMatch.waitForExistence(timeout: 30))
// Restore the start through public gestures; last-graph persistence and
// virtualization of picker rows must not make test ordering significant.
for _ in 0..<3 {
if title(1, app).exists && title(1, app).isHittable { break }
swipe(app)
}
XCTAssertTrue(title(1, app).waitForExistence(timeout: 20))
gone(app)
return app
}
@MainActor private func title(_ graph: Int, _ app: XCUIApplication) -> XCUIElement {
app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "Graph \(graph) —")).firstMatch
}
@MainActor private func capsule(_ app: XCUIApplication) -> XCUIElement {
app.staticTexts["graph-switch-name"]
}
@MainActor private func swipe(_ app: XCUIApplication, short: Bool = false) {
// The list's top blank region avoids rows and the existing floating controls.
app.coordinate(withNormalizedOffset: CGVector(dx: 0.15, dy: 0.15))
.press(forDuration: 0.05, thenDragTo: app.coordinate(withNormalizedOffset:
CGVector(dx: short ? 0.29 : 0.78, dy: 0.15)), withVelocity: .slow, thenHoldForDuration: 0)
}
@MainActor private func record(_ name: String, _ app: XCUIApplication) {
let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
screenshot.name = name; screenshot.lifetime = .keepAlways; add(screenshot)
let tree = XCTAttachment(string: app.debugDescription)
tree.name = name + " accessibility"; tree.lifetime = .keepAlways; add(tree)
}
@MainActor private func gone(_ app: XCUIApplication) {
let absent = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: capsule(app))
XCTAssertEqual(XCTWaiter.wait(for: [absent], timeout: 2), .completed)
}
@MainActor func testNameCommitAndFade() {
let app = host()
XCTAssertTrue(title(1, app).waitForExistence(timeout: 20))
XCTAssertFalse(capsule(app).exists)
swipe(app)
XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5), "A right swipe must identify its target")
XCTAssertEqual(capsule(app).label, "Native encrypted graph 2")
XCTAssertTrue(title(2, app).waitForExistence(timeout: 5))
record("Graph 2 name after real native commit", app)
gone(app)
record("Name fades after commit", app)
}
@MainActor func testCancellationAndContinuousCycle() {
let app = host()
XCTAssertTrue(title(1, app).waitForExistence(timeout: 20))
swipe(app, short: true)
gone(app)
XCTAssertTrue(title(1, app).isHittable)
for graph in [2, 3, 1] {
swipe(app)
XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5))
XCTAssertEqual(capsule(app).label, "Native encrypted graph \(graph)")
XCTAssertTrue(title(graph, app).waitForExistence(timeout: 5))
record("Consecutive graph \(graph)", app)
}
gone(app)
}
@MainActor func testDarkAppearanceAndTouchThrough() {
let app = host(dark: true)
XCTAssertTrue(title(1, app).waitForExistence(timeout: 20))
swipe(app)
XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5))
XCTAssertEqual(capsule(app).label, "Native encrypted graph 2")
record("Dark capsule", app)
// The overlay must not consume touches destined for the production page.
app.buttons["Options"].firstMatch.tap()
XCTAssertTrue(app.buttons["Switch graph"].waitForExistence(timeout: 3))
record("Menu works while name is visible", app)
}
@MainActor func testLongNameStaysClearOfMenu() {
let app = host(longName: true)
XCTAssertTrue(title(1, app).waitForExistence(timeout: 20))
swipe(app)
XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5))
XCTAssertEqual(capsule(app).label, "Reading & Research — 工作笔记 and long-term notes")
XCTAssertLessThan(capsule(app).frame.maxX, app.buttons["Options"].firstMatch.frame.minX - 4)
XCTAssertGreaterThan(capsule(app).frame.minX, app.frame.minX + 16)
let progress = app.activityIndicators.firstMatch
if progress.exists {
XCTAssertLessThanOrEqual(capsule(app).frame.maxX + 16, progress.frame.minX)
}
record("Long name preserves full accessibility text", app)
gone(app)
}

@MainActor func testAccessibilitySizePreservesDateClearance() {
let app = host(accessibility: true)
swipe(app)
XCTAssertTrue(capsule(app).waitForExistence(timeout: 0.5))
// Native List's date accessibility frame includes top padding rather than
// tight glyph bounds. Verify the HUD stays in the existing control band;
// the retained screenshot supplies the date-ink clearance check.
let controls = app.buttons["Options"].firstMatch.frame
XCTAssertGreaterThanOrEqual(capsule(app).frame.minY, controls.minY)
XCTAssertLessThanOrEqual(capsule(app).frame.maxY, controls.maxY)
record("Accessibility size keeps date clear", app)
}

}
53 changes: 53 additions & 0 deletions apple-tests/native-list/JournalGraphNameStateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

@main struct JournalGraphNameStateTests {
static func main() {
var state = JournalGraphNameFeedback()
state.show(source: "account:A:1", target: "account:B:2", name: "Work — 工作笔记")
let preview = state.presentation!
precondition(preview.name == "Work — 工作笔记" && preview.phase == .preview)
state.focusChanged("account:B:2")
precondition(state.presentation == nil, "An uncommitted external focus is not a successful swipe")
state.show(source: "A", target: "B", name: "Work")
state.commit(source: "A", target: "C")
precondition(state.presentation?.phase == .preview, "Wrong target cannot confirm a request")
state.commit(source: "A", target: "B")
precondition(state.presentation?.phase == .waiting)
state.focusChanged("A")
precondition(state.presentation?.phase == .waiting, "Save may defer focus without a timeout")
let waitingID = state.presentation!.id
state.focusChanged("B")
let confirmed = state.presentation!
precondition(confirmed.phase == .confirmed && confirmed.id != waitingID)
state.show(source: "B", target: "C", name: "Reading")
state.dismiss(confirmed.id)
precondition(state.presentation?.name == "Reading", "An earlier fade must not hide the next swipe")
state.cancel()
state.focusChanged("C")
precondition(state.presentation == nil, "Cancellation rejects a late focus")
state.show(source: "A", target: "B", name: "Work")
state.commit(source: "A", target: "B")
state.focusChanged("C")
precondition(state.presentation == nil, "External selection clears a pending target")
state.show(source: "A:1", target: "B:1", name: "Work")
state.retain(["A:1", "B:2"])
precondition(state.presentation == nil, "Retired graph generations cannot retain a name")
for name: String? in [nil, "", " \n "] {
state.show(source: "A", target: "B", name: name)
precondition(state.presentation == nil, "Missing names must never reveal graph IDs")
}
state.show(source: "A", target: "B", name: "Work")
state.updateName(target: "C", name: "Other")
precondition(state.presentation?.name == "Work")
state.updateName(target: "B", name: "Renamed work")
precondition(state.presentation?.name == "Renamed work")
state.commit(source: "A", target: "B")
state.focusChanged("B")
state.dismiss(state.presentation!.id)
precondition(state.presentation == nil)
state.show(source: "A", target: "B", name: "Work")
state.retain([])
precondition(state.presentation == nil, "Account exit clears a visible name")
print("PASS graph-name feedback lifecycle")
}
}
19 changes: 19 additions & 0 deletions apple-tests/native-list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,22 @@ SwiftUI `ScrollPosition` did not restore this List in the same failing case, so
restoration reuses the existing resolved `UIScrollView` and native offset API.
`JournalListSnapshotTests.swift` additionally verifies runtime separation,
resident-generation retirement and rejection of late offset writes after logout.

## Graph name feedback

`JournalGraphNameAcceptance.swift` drives the actual native right-swipe capsule
through an isolated three-resident offline warm-start host. Supply its bundle ID
via `JOURNAL_GRAPH_NAME_APP` (default `org.logseq.journal.graph-name-probe`).
Install generated `valid.json` and `support-valid` in that bundle's Documents;
for the long-name case use `long.json` and an independent `support-long` with
catalog graph 2 named `Reading & Research — 工作笔记 and long-term notes`.
The tests cover target identity, commit/fade, cancellation, a full resident
cycle, native dark appearance, long-name bounds and touches reaching Options.
Transient timing and smooth page motion also require local screen-recording
inspection; XCTest idle waits alone are not a timing/performance measurement.

Run `python3 tool/test_graph_name.py` for deterministic native feedback ownership:
delayed focus, cancellation, stale fade IDs, external selection, catalog names
and retired graph/account scopes. This small native presentation owner is
outside the OCaml reducer; catalog-to-native name transport is covered once at
the real Application renderer in `application_view_test.ml`.
Loading
Loading