Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
054d54c
Plan: Bot chat activity slice D (phones)
sambitcreate Oct 14, 2026
3ba1c3f
Remote revision 30: Bot activity, queued and chatActivity wire shapes…
sambitcreate Oct 14, 2026
5fe9554
Bots: shared Stop and Resume intents for the Mac and phones
sambitcreate Oct 14, 2026
2c6f1f5
Bots: turn-intent listener errors are logged; per-Bot Stop isolation …
sambitcreate Oct 14, 2026
87b1840
Remote: Bot activity and queued frames for bot:activity devices; Stee…
sambitcreate Oct 14, 2026
5105130
Remote: chatActivity on Bot detail and PATCH; bot-activity-v1 and the…
sambitcreate Oct 14, 2026
26576a2
iOS Bots: revision 30 activity wire, bot:activity opt-in, shared stat…
sambitcreate Oct 14, 2026
7ebf60f
Remote: every Bot detail route strips chatActivity for devices withou…
sambitcreate Oct 14, 2026
0448313
iOS Bots: status line state, Reconnecting, queued messages and Steer/…
sambitcreate Oct 14, 2026
4f06643
iOS Bots: status line, Steer/Queue pill, Queued bubbles and glancing …
sambitcreate Oct 14, 2026
0f873b1
Remote: user transcript entries carry their submissionId for bot:acti…
sambitcreate Oct 14, 2026
aead8c7
iOS Bots: exact outbox reconcile, a durable outbox and instant turn c…
sambitcreate Oct 14, 2026
eba7b1b
Remote: submissionId tags are sticky while an entry is in view
sambitcreate Oct 14, 2026
0c1e916
iOS Bots: Advanced → Chat activity with looping previews, saved on pick
sambitcreate Oct 14, 2026
d3b9444
Android Bots: revision 30 activity wire, bot:activity opt-in, shared …
sambitcreate Oct 14, 2026
ee4c1ad
Android Bots: status line state, Reconnecting, queued messages and St…
sambitcreate Oct 14, 2026
0d1ff3b
iOS Bots: rollback by field, no silent drops, owned outbox writes
sambitcreate Oct 14, 2026
a66cf3d
Android Bots: Task 8 review folds for the activity wire
sambitcreate Oct 14, 2026
c329f27
Android Bots: status line, Steer/Queue split button, Queued bubbles a…
sambitcreate Oct 14, 2026
2da9793
iOS Bots: Chat activity recovers from conflicts, pauses previews, ope…
sambitcreate Oct 14, 2026
77e8f7a
Android Bots: frozen requests, no silent drop on exhaustion, outbox r…
sambitcreate Oct 14, 2026
58b7b15
Android Bots: Chat activity model and looping previews, saved on pick
sambitcreate Oct 14, 2026
8c52782
Android Bots: Chat activity reports late saves, refreshes after timeo…
sambitcreate Oct 14, 2026
375c2f4
iOS Bots: no silent drop on exhaustion, bounded requests, ordered out…
sambitcreate Oct 14, 2026
35c9192
Android Bots: Advanced → Chat activity page, row, and callers (Sessio…
sambitcreate Oct 14, 2026
cb40568
Docs: Bot chat activity slice D rulings, revision 30 contract, plan i…
sambitcreate Oct 14, 2026
33dd249
Android Bots: Chat activity cache stays pinned to its pairing; cold o…
sambitcreate Oct 14, 2026
662e34a
iOS Bots: failed messages never settle on a tagged entry; key reuse s…
sambitcreate Oct 14, 2026
7049b04
iOS Bots: a refused message retries on a new key; unreadable outbox kept
sambitcreate Oct 14, 2026
4061cf7
Fixture: null-literal rejected samples for revision 30
sambitcreate Oct 14, 2026
249e97c
Remote Bots: a send refused from state leaves no ledger record (R22b)
sambitcreate Oct 14, 2026
c927462
Protocol test: narrow the null-literal entry before reading its id
sambitcreate Oct 14, 2026
e2f6780
Android Bots: unreadable outboxes set aside, bounded requests, write-…
sambitcreate Oct 14, 2026
6510351
Bots: one run start per process, shared by the desktop feed and phone…
sambitcreate Oct 14, 2026
b0d8cfc
iOS Bots: final wave — snapshot grace, cached capabilities, still bad…
sambitcreate Oct 14, 2026
dc7a1bf
Bots: one fail-closed queued classifier for desktop and phones; queue…
sambitcreate Oct 14, 2026
2161fd1
Remote router test: a held bot:activity grant gets activity only whil…
sambitcreate Oct 14, 2026
a494577
Remote Bots: a failing placement lookup is reported once per streak; …
sambitcreate Oct 14, 2026
04b6234
iOS Bots: New Bot, Profile, Memory and Advanced apply at once (R15)
sambitcreate Oct 14, 2026
7533f20
Android Bots: persisted activity support, pairing-pinned saves, chat …
sambitcreate Oct 14, 2026
a9a4674
Docs: Bot chat activity final fix wave, desktop and protocol outcomes
sambitcreate Oct 14, 2026
fd029a3
Docs: Bot chat activity final fix wave, iOS outcomes
sambitcreate Oct 14, 2026
237e9e8
Android Bots: slice-A edits apply at once (R15), unheld grace as on iOS
sambitcreate Oct 14, 2026
1672ed9
Docs: Android final-wave outcomes; memory note placement
sambitcreate Oct 14, 2026
663d6f3
Merge remote-tracking branch 'origin/main' into claude/bot-chat-activ…
sambitcreate Oct 14, 2026
bbd1327
Remote Bots: a model that can't run is a 409 refusal from state, not …
sambitcreate Oct 14, 2026
e0202df
Android Bots: a restore that lands after an access-loss purge is dropped
sambitcreate Oct 14, 2026
f78443b
Remote Bots: cap queued and snapshot bytes under the SSE frame limit
sambitcreate Oct 14, 2026
dbdf9ae
iOS release policy: match the Bot chat and Advanced catalog code as i…
sambitcreate Oct 14, 2026
7aebccf
Remote Bots: allocate the snapshot budget in priority order by constr…
sambitcreate Oct 14, 2026
65e0096
iOS Bots: route outbox failures through the typed diagnostics wrapper
sambitcreate Oct 15, 2026
cc7ae5c
iOS Bots: a send before the outbox restore never replaces the last la…
sambitcreate Oct 15, 2026
9a2feca
iOS Bots: Try Again replays the frozen Create through a catalog change
sambitcreate Oct 15, 2026
25574c1
iOS Bots: Chat activity picks stay with the pairing they were made on
sambitcreate Oct 15, 2026
573298e
iOS Bots test: seed the last launch's outbox through a model, not a f…
sambitcreate Oct 15, 2026
7744dd3
iOS Bots: a restore that lands after an access-loss purge is dropped
sambitcreate Oct 15, 2026
3d3cbc9
Test harness: contain a late EPIPE when the worktree editor exits bef…
sambitcreate Oct 15, 2026
ea29ffe
Android Bots: refuse the fixture's null literals (app, queued, label …
sambitcreate Oct 15, 2026
6352bfb
Android Bots: unconfirmed edits are shown, never written to the Bot c…
sambitcreate Oct 15, 2026
9685204
iOS Bots: a New Bot cut off by a relaunch comes back with Try Again
sambitcreate Oct 15, 2026
c47d018
Android Bots: New Bot closes at once and shows Creating… on Bots home…
sambitcreate Oct 15, 2026
4de4bf3
Android Bots: inline error text reads at 4.5:1 in every theme
sambitcreate Oct 15, 2026
212f692
Android Bots test: Bots home shows a Bot on its way and Try Again reo…
sambitcreate Oct 15, 2026
7eddc5b
iOS Bots: the Files sheet loads with skeleton rows, not a spinner
sambitcreate Oct 15, 2026
e348b01
Android Bots: a New Bot cut off by a process death comes back with Tr…
sambitcreate Oct 15, 2026
d4d6358
iOS Bots test: every Chat activity hold is bounded by a deadline
sambitcreate Oct 15, 2026
978caa6
iOS Bots: a Chat activity pick made while a save timed out is not dro…
sambitcreate Oct 15, 2026
4030c06
iOS Bots: a Chat activity pick still reports when the sheet closes fr…
sambitcreate Oct 15, 2026
89b91a0
iOS Bots: a restored New Bot that fails again still replays on Try Again
sambitcreate Oct 15, 2026
9c35409
Docs: record the pre-merge bug sweep for Bot chat activity D
sambitcreate Oct 15, 2026
0f43fe0
Android Bots: a New Bot goes out only once its request and key are on…
sambitcreate Oct 15, 2026
6b7b8c0
Android Bots: a restored New Bot stays with the pairing it was made on
sambitcreate Oct 15, 2026
ba980b9
Android Bots: an ambiguous New Bot is never replayed past the Mac's day
sambitcreate Oct 15, 2026
edfc8f0
iOS Bots: a recovered New Bot stays on the Mac it was made for
sambitcreate Oct 15, 2026
2859984
iOS Bots: a lost New Bot is never sent again on a key the Mac forgot
sambitcreate Oct 15, 2026
d7a9d5e
iOS Bots: a New Bot that can't be written to disk is never sent
sambitcreate Oct 15, 2026
492eb1a
iOS Bots: a Try Again while another New Bot is on its way keeps its a…
sambitcreate Oct 15, 2026
69f5c2f
Chat: a run this window started is never mirrored back to it
sambitcreate Oct 15, 2026
4813a5c
Chat: Steer and Queue work while a response is still starting
sambitcreate Oct 15, 2026
a702a26
E2E: chat queue specs report what failed upstream
sambitcreate Oct 15, 2026
f7b919c
Memory: chat-message-queue flake investigation and fixes
sambitcreate Oct 15, 2026
22bc147
Pi: a Stop-interrupted journal write is not logged as a failure
sambitcreate Oct 15, 2026
fee9ae3
Chat transition test: follow the send path now that generation starts…
sambitcreate Oct 15, 2026
f7f8284
E2E: queued image follow-ups are held through each phase of a startin…
sambitcreate Oct 15, 2026
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
2 changes: 1 addition & 1 deletion .memory/android-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ All user-visible Android copy in composables lives in `android/app/src/main/res/
- Composables use `stringResource` / `pluralStringResource`. Copy built inside `semantics {}`, `clearAndSetSemantics {}`, non-composable label lambdas (`AidenSegmentedPillRow`'s `label`), or `CustomAccessibilityAction` is hoisted into a `val` first. Copy built in coroutines or local functions uses `val resources = LocalResources.current` and `resources.getString`. Notifications use `Context.getString`.
- Counts use `<plurals>`; sentences take `%1$s` / `%1$d` arguments rather than concatenation. Small enums and private label helpers carry `@StringRes` ids (`AidenUsageTokenKind`, `AidenBotEditorAccessMode`, `AidenBotChatAccessScope`, `AidenRunInputModeOption`, `AgentRunActivityStatus.titleRes`).
- English values stay byte-identical to what UI tests match with `onNodeWithText`. `chat_progress_steps_done` keeps "steps" and seven placeholders keep "..." with `tools:ignore` for that reason.
- Deliberately still English: ViewModel / controller / state-holder error fallbacks (`AidenChatViewModel`, `AidenWorkspaceHomeViewModel`, `AidenBotsViewModel`, `ComposerVoiceInputController`, `AidenBotGeneratedAvatarModel`, `AidenBotChatToolsModel`, `AidenChatForkErrors`); JVM-tested presentation helpers (`AidenChatRowStatusPresentation`, `aidenRelativeTimestamp`, `aidenFolderCrumbs`, Usage date/inspection labels, `AidenReadAloudProgress.label`, `AidenImageCountBadge.label`, `AidenTurnElapsed`, `AidenMessageTimestamp`); model-layer titles (`AidenWorkspacePermission`, `AidenScheduledTaskFilter`, `AidenApprovalPresentation`, `AidenScheduledTaskPresentation`); protocol values, ids, test tags, and the dev-only `features/bots/prototype`. Moving these needs a resource-id contract change and test updates.
- Deliberately still English: ViewModel / controller / state-holder error fallbacks (`AidenChatViewModel`, `AidenWorkspaceHomeViewModel`, `AidenBotsViewModel`, `ComposerVoiceInputController`, `AidenBotGeneratedAvatarModel`, `AidenBotChatToolsModel`, `AidenChatForkErrors`); JVM-tested presentation helpers (`AidenChatRowStatusPresentation`, `aidenRelativeTimestamp`, `aidenFolderCrumbs`, Usage date/inspection labels, `AidenReadAloudProgress.label`, `AidenImageCountBadge.label`, `AidenTurnElapsed`, `aidenBotElapsedLabel` and `aidenBotDurationLabel` (Bot status line elapsed time, `AidenBotActivity.kt`), `AidenMessageTimestamp`); model-layer titles (`AidenWorkspacePermission`, `AidenScheduledTaskFilter`, `AidenApprovalPresentation`, `AidenScheduledTaskPresentation`); protocol values, ids, test tags, and the dev-only `features/bots/prototype`. Moving these needs a resource-id contract change and test updates.
51 changes: 50 additions & 1 deletion .memory/bot-chat-activity.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .memory/bots-rework-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ See `.memory/bot-chat-activity.md` for the status line, Chat activity and recaps

## Feature tokens and revision
- Revision **27** (2026-10-09, Bot memory/soul/proactivity, re-claim at merge; `main` was 26): tokens `bot-memory-v1` and `bot-proactive-v1`, negotiable `bot:cards` gating `memory_update`/`routine_proposal` session entries, routes for memory, proposal answers, routine suggestions and `GET /bots/routine-notifications`, and `openingGreeting` retired (never emitted, accepted and ignored). Details: `.memory/bot-proactivity.md`.
- Revision **30** (2026-10-14, Bot chat activity on phones, provisional, re-claim at merge; `main` was 29): token `bot-activity-v1`, negotiable `bot:activity` gating the `activity` and `queued` session frames and snapshot keys, `submissionId` on the person's user entries, and Bot detail `chatActivity`; `PATCH /bots/{id}` accepts `chatActivity` and `POST /bots/{id}/messages` accepts `whenBusy: steer|queue` only while the token is announced. Phones keep a persisted, TTL'd, file-protected outbox of unsent text. Details: `.memory/bot-chat-activity.md`; contract: `docs/aiden-remote-api-v1.md`.
- Aiden Remote contract revision **25** (`main` is at 24). Tokens in `aiden-remote-protocol.ts`: `bot-delete-v1`, `bot-durable-session-v1`, `bot-routines-v1`, `bot-connection-requests-v1`, `bot-presets-v1`. iOS (`AidenBotSessionModel.swift`) and Android consume the same tokens, and fixtures are shared across TS, Swift and Kotlin.
- Routes added in revision 25: `POST /bots/{id}/resume` and `/dismiss` (idempotent per request UUID), `DELETE /bots/{id}`, `GET/POST/PATCH/DELETE /bots/{id}/routines`, `POST /bots/{id}/connection-requests`, `GET /bot-presets`, `POST /bots/from-preset`.
- Remote audit (2026-10-07): `/server.features` announces each token only while its route is wired (router test "revision 25 Bot feature tokens…"; before this the tokens were never sent). `GET /bots/{id}/session` and `/session/events` need `chat:read` as well as `bot:read`; the stream crosses the revocation fence and `revokeDevice` closes it; a rebuilt Remote API closes the old session service.
Expand Down
17 changes: 17 additions & 0 deletions .memory/revisited-generation-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ Hosted verification also exercised the broader Environment/Subagents source cont
Focused renderer/IPC tests, chat/composer suite, and held-response Electron regressions cover revisit → queue/steer → next turn and revisit → Stop → new turn. E2E held-response sidebar titles reflect the original prompt, not the deterministic completion text. Local native helper scripts discard inherited SDK variables; on this host a direct Xcode 26.5 SDK helper build was used for E2E because the CommandLineTools macOS 27 SDK fails linking.

Review: OpenCode Workers created an isolated checkout at the exact feature commit, but its launcher rejected `--dir` under the configured OpenCode v2 CLI. A direct read-only DeepSeek V4 Flash run in that same worktree reviewed the full diff and surrounding lifecycle/queue contracts, reported no actionable findings, and left the worktree unchanged. The requested “4.1 Flash” name was not listed by OpenCode; V4 Flash was used as the available model.

## October 15: chat-message-queue CI flakes (PR #479)

Two held-response specs flake about once per few dozen hosted runs and never locally: "Stop after revisiting…" (Stop missing after the revisit, line ~617; sibling "revisiting a running chat…" had the same symptom on Oct 9) and "a response finishing while the queue editor is open…" (0 requests with the edited text, line ~511). They predate Bot slice C2 (#478): the editor test flaked on Oct 10 and Stop-after-revisit on Oct 13 (mcp-07 branch, head bcc1845d) — so C2's route memory, Bot panel provider and Bot notification sync are ruled out.

Not reproduced: ~1,300 local runs (macOS, 8 workers, CPU stress, CDP 20x renderer throttle, background QoS, SIGSTOP stalls of main/renderer, slow fs, delayed IPC replies and idle notifications, focus loss, gap sweeps) plus 360 runs in a Linux container under xvfb. Reading the code, both symptoms need the first held response to end (a terminal or error) while the test thinks it is still running: a terminal hides and drops the detached projection (Stop gone), and `onError` pauses the chat queue (edited row never sent). The trigger on hosted runners is unknown.

Fixed along the way: after a revisited chat's detached run ended, the watcher started on revisit mirrored that same run back as an "observed" (phone) run because the only ownership check was the detached marker the terminal had just cleared. The composer re-locked ("Waiting for the response…") right after unlocking, for 10–300 ms (11 of 12 local probe runs; 0 of 12 after). `rememberOwnGenerationStream` in `chat-terminal-sync.ts` (called from `startGeneration` and on detach) now keeps a bounded set of this document's runs that are never mirrored.

Diagnostics for the next hosted failure: the E2E fixture prints, on failure, the on-screen alerts/status/toasts and the dev log's warn/error records to stderr (the reporter truncates the dev-log attachment, and hosted artifacts keep only the sanitized receipt). The editor spec also asserts the first reply saved and the queue is not paused before Save, so an upstream failure no longer reads as an editor bug.

Also found by loading the whole spec (6 workers locally), where the Steer specs failed 8 of 30 before and 0 of 60 after (whole spec, 4 workers, repeat-each 30: 570 of 570):
- Steer during the start window was rejected. Stop and the Steer pill appear as soon as a send begins, but Pi opens its run-input queue only at `agent_start`, so main answered `run_not_active` and the composer said "The current response has ended". `admitRunInputOnceStarted` (`chat-message-queue.ts`) now retries `run_not_active` while the pane's own run is still live (bounded to about 8.5 s; a rejected attempt writes nothing). Detached runs are not retried.
- `handleSend` awaited the `chat:start` acknowledgement. That held the composer's `sending` flag, so the busy pill's `canSubmit` was false while main initialized, and picking Steer or Queue only switched modes. The send now finishes at the durable append; a start rejection still sets the pane error.
- Test race: after picking from the busy pill's menu, Radix returns focus to the chevron a task later, so a keypress sent straight to the composer could land on the trigger and reopen the menu. The rejected-steer spec now waits for that focus first.

Noise seen in every local run: "Could not append Pi session messages for stream …" logged three times at error level on each user Stop.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package sbtbiswas.AidenOnTheGo.features.bots

import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import sbtbiswas.AidenOnTheGo.models.AidenBotSession
import sbtbiswas.AidenOnTheGo.models.AidenBotSessionActivity
import sbtbiswas.AidenOnTheGo.models.AidenBotSessionState
import sbtbiswas.AidenOnTheGo.ui.theme.AidenTheme

/** What TalkBack hears from the Bot chat's status line and its unsent messages. */
@RunWith(AndroidJUnit4::class)
class AidenBotChatSemanticsUiTest {
@get:Rule
val compose = createComposeRule()

private fun running(startedAt: Long) = AidenBotSession(
botId = "bot_fixture_01",
epoch = "epoch_1",
seq = 1,
state = AidenBotSessionState.RUNNING,
interrupted = false,
entries = emptyList(),
hasOlder = false,
question = null,
approval = null,
activity = AidenBotSessionActivity("reading", null, startedAt)
)

// FR-m8: the word is the line's name and the elapsed time its state, as on iOS.
@Test
fun theStatusLineIsNamedByItsWordWithTheElapsedTimeAsItsState() {
val ui = AidenBotSessionUiState(session = running(System.currentTimeMillis() - 75_000), activitySupported = true, hasLoaded = true)
// The mark and the elapsed clock tick forever: hold the clock so the test can settle.
compose.mainClock.autoAdvance = false
compose.setContent { AidenTheme { AidenBotStatusLine(ui) } }
compose.mainClock.advanceTimeBy(1_000)
val node = compose.onNodeWithTag(AidenBotStatusLineTags.LINE).fetchSemanticsNode()
assertEquals(listOf("Reading"), node.config.getOrNull(SemanticsProperties.ContentDescription))
val state = node.config.getOrNull(SemanticsProperties.StateDescription)
assertTrue("elapsed state was $state", state != null && Regex("^1m 1[5-7]s$").matches(state))
}

@Test
fun underTwentySecondsTheLineHasNoElapsedState() {
val ui = AidenBotSessionUiState(session = running(System.currentTimeMillis() - 5_000), activitySupported = true, hasLoaded = true)
// The mark and the elapsed clock tick forever: hold the clock so the test can settle.
compose.mainClock.autoAdvance = false
compose.setContent { AidenTheme { AidenBotStatusLine(ui) } }
compose.mainClock.advanceTimeBy(1_000)
compose.onNodeWithTag(AidenBotStatusLineTags.LINE)
.assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.StateDescription))
}

// A failed message says so as its state, and Try again is its own control.
@Test
fun aFailedMessageCarriesItsStateAndOffersTryAgain() {
val message = AidenBotOutgoingMessage.create("Plan lunch", null, enqueuedWhileRunning = false, retryOf = null, nowMs = 0)
.copy(status = AidenBotOutgoingStatus.FAILED)
var retried = 0
compose.setContent {
AidenTheme {
AidenBotOutgoingBubble(message, AidenBotOutgoingLook.Failed, canRetry = true, onRetry = { retried += 1 }, onRemove = {})
}
}
compose.onNodeWithText("Plan lunch", useUnmergedTree = false)
.assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "Couldn’t send"))
compose.onNodeWithText("Try again").assertHasClickAction().performClick()
compose.runOnIdle { assertEquals(1, retried) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sbtbiswas.AidenOnTheGo.features.bots

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import sbtbiswas.AidenOnTheGo.auth.InMemoryAidenSecureStore
import sbtbiswas.AidenOnTheGo.features.remote.AidenRemoteCoordinator
import sbtbiswas.AidenOnTheGo.persistence.AidenInstallationStore
import sbtbiswas.AidenOnTheGo.ui.theme.AidenTheme
import java.io.File

/** R15: New Bot closes at once; Bots home shows the Bot on its way, and a failure offers Try Again. */
@RunWith(AndroidJUnit4::class)
class AidenBotCreatePendingUiTest {
@get:Rule
val compose = createComposeRule()

private lateinit var storage: File
private lateinit var coordinator: AidenRemoteCoordinator

@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
storage = File(context.cacheDir, "bot-create-pending-${System.nanoTime()}").apply { mkdirs() }
coordinator = AidenRemoteCoordinator(
installationStore = AidenInstallationStore(storage, InMemoryAidenSecureStore()),
storageDir = storage,
// Never connected: no Mac answers, so a create can only wait and fail.
scope = CoroutineScope(Dispatchers.Unconfined + Job().apply { cancel() })
)
}

@After
fun tearDown() {
storage.deleteRecursively()
}

@Test
fun aBotOnItsWayShowsOnBotsHomeAndAFailureReopensNewBotAsItWas() {
val flow = coordinator.botCreateFlow()
val viewModel = compose.runOnIdle { AidenBotsViewModel(coordinator, coordinator.botCache) }
var reopened = 0
compose.setContent {
AidenTheme {
AidenBotsHomeScreen(
coordinator = coordinator,
viewModel = viewModel,
onNavigateToChat = {},
onNavigateToBotProfile = {},
onNavigateToCreateBot = { reopened += 1 }
)
}
}

val draft = AidenBotCreateDraft(name = "Meal Planner", help = "Plans dinners")
val attempt = compose.runOnIdle { flow.start(draft, null)!! }
compose.onNodeWithText("Meal Planner").assertIsDisplayed()
compose.onNodeWithText("Creating…").assertIsDisplayed()

// No Mac to reach: the row goes and the failure says so.
runBlocking { flow.send(attempt) }
compose.onNodeWithText("Creating…").assertDoesNotExist()
compose.onNodeWithText("Couldn’t Create Bot").assertIsDisplayed()
compose.onNodeWithText("Try Again").performClick()

compose.runOnIdle {
assertEquals(1, reopened)
assertEquals("New Bot opens with the same answers", draft, flow.reopening())
}
compose.onNodeWithText("Couldn’t Create Bot").assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class AidenAppContainer(context: Context) {
draftStore = draftStore,
navigationStore = navigationStore,
intentCatalogStore = intentCatalogStore,
scope = scope
scope = scope,
botOutboxDir = appContext.noBackupFilesDir
)
private val settingsCache = AidenSettingsCache(File(filesDir, "settings_cache"))
val settingsStore = AidenSettingsStore(settingsCache, scope)
Expand Down
Loading
Loading