diff --git a/apps/mobile/plugins/withAndroidFbjniVersion.test.mjs b/apps/mobile/plugins/withAndroidFbjniVersion.test.mjs index 388afbf3f67d7..c221c14ca4470 100644 --- a/apps/mobile/plugins/withAndroidFbjniVersion.test.mjs +++ b/apps/mobile/plugins/withAndroidFbjniVersion.test.mjs @@ -86,6 +86,7 @@ describe("withAndroidFbjniVersion", () => { expect(twice).toBe(once); }); + // regression: #20118 fresh Android builds no longer crash on launch it("writes Kotlin syntax into a Kotlin build script", async () => { const result = await prebuildRootGradle("build.gradle.kts", "plugins {}\n"); diff --git a/apps/mobile/src/components/ui/headerMenu.test.ts b/apps/mobile/src/components/ui/headerMenu.test.ts deleted file mode 100644 index d9ffb33b10640..0000000000000 --- a/apps/mobile/src/components/ui/headerMenu.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; -import { splitAndroidHeaderActions } from "./headerMenu.android"; - -describe("header menus", () => { - it("keeps overflow-only Android header actions in the overflow menu", () => { - const action = (accessibilityLabel: string, overflowOnly?: boolean) => ({ - accessibilityLabel, - icon: "trash" as const, - onPress: () => {}, - overflowOnly, - }); - const labels = ( - split: ReturnType>>, - ) => ({ - visible: split.visibleActions.map((item) => item.accessibilityLabel), - overflow: split.overflowActions.map((item) => item.accessibilityLabel), - }); - const open = action("Open"); - const remove = action("Delete", true); - expect(labels(splitAndroidHeaderActions([open, action("Comment")], false))).toEqual({ - visible: ["Open", "Comment"], - overflow: [], - }); - expect(labels(splitAndroidHeaderActions([open, remove], true))).toEqual({ - visible: ["Open"], - overflow: ["Delete"], - }); - expect(labels(splitAndroidHeaderActions([remove], true))).toEqual({ - visible: [], - overflow: ["Delete"], - }); - expect( - labels(splitAndroidHeaderActions([action("Review"), action("Comment"), open, remove], true)), - ).toEqual({ visible: ["Review", "Comment", "Open"], overflow: ["Delete"] }); - }); -}); diff --git a/apps/mobile/src/connection/environment-cache-store.test.ts b/apps/mobile/src/connection/environment-cache-store.test.ts index 376bc6c60e4ec..4c88a03ccf2a0 100644 --- a/apps/mobile/src/connection/environment-cache-store.test.ts +++ b/apps/mobile/src/connection/environment-cache-store.test.ts @@ -11,7 +11,6 @@ import { type OrchestrationV2ShellSnapshot, type OrchestrationV2ThreadDetailSnapshot, type ThreadPullRequestLink, - type VcsListRefsResult, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; @@ -144,20 +143,6 @@ const THREAD_SNAPSHOT: OrchestrationV2ThreadDetailSnapshot = { updatedAt: NOW, }, }; -const REFS: VcsListRefsResult = { - refs: [ - { - name: "main", - current: true, - isDefault: true, - worktreePath: "/repo", - }, - ], - isRepo: true, - hasPrimaryRemote: true, - nextCursor: null, - totalCount: 1, -}; function cacheId( environmentId: EnvironmentId, @@ -334,21 +319,4 @@ describe("mobile SQLite environment cache store", () => { expect(memory.removed).toEqual([id]); }), ); - - it.effect("clears one environment without touching another", () => - Effect.gen(function* () { - const memory = makeDatabase(); - const store = yield* make().pipe( - Effect.provideService(MobileDatabase.MobileDatabase, memory.database), - ); - const otherEnvironmentId = EnvironmentId.make("environment-2"); - yield* store.saveVcsRefs(ENVIRONMENT_ID, "/repo", REFS); - yield* store.saveVcsRefs(otherEnvironmentId, "/repo", REFS); - - yield* store.clear(ENVIRONMENT_ID); - - expect(yield* store.loadVcsRefs(ENVIRONMENT_ID, "/repo")).toEqual(Option.none()); - expect(yield* store.loadVcsRefs(otherEnvironmentId, "/repo")).toEqual(Option.some(REFS)); - }), - ); }); diff --git a/apps/mobile/src/features/agent-awareness/notificationNavigation.test.ts b/apps/mobile/src/features/agent-awareness/notificationNavigation.test.ts index 9a4b2df27be4f..92512bd2c3e87 100644 --- a/apps/mobile/src/features/agent-awareness/notificationNavigation.test.ts +++ b/apps/mobile/src/features/agent-awareness/notificationNavigation.test.ts @@ -26,28 +26,6 @@ afterEach(() => { }); describe("consumeLastAgentNotificationResponse", () => { - it("routes a response before reporting a clear failure", async () => { - const cause = new Error("notification clear unavailable"); - const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); - const response = responseWithData({}, "notification-clear") as NotificationResponse; - const handleResponse = vi.fn(); - - await consumeLastAgentNotificationResponse({ - getLastResponse: () => Promise.resolve(response), - clearLastResponse: () => Promise.reject(cause), - handleResponse, - }); - - expect(handleResponse).toHaveBeenCalledWith(response); - expect(consoleError).toHaveBeenCalledWith( - expect.objectContaining({ - _tag: "NotificationNavigationError", - operation: "clear", - notificationId: "notification-clear", - }), - ); - }); - it("reports routing failures before clearing the response", async () => { const cause = new Error("notification routing unavailable"); const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); @@ -86,16 +64,6 @@ describe("extractAgentNotificationDeepLink", () => { ).toBe("/threads/env/thread"); }); - it("normalizes explicit thread deep links from APNs payload data", () => { - expect( - extractAgentNotificationDeepLink( - responseWithData({ - deepLink: "/threads/env%201/thread%2F2", - }), - ), - ).toBe("/threads/env%201/thread%2F2"); - }); - it("falls back to the thread route from environment and thread ids", () => { expect( extractAgentNotificationDeepLink( diff --git a/apps/mobile/src/features/agent-awareness/notificationPermissions.test.ts b/apps/mobile/src/features/agent-awareness/notificationPermissions.test.ts deleted file mode 100644 index 94d265788247b..0000000000000 --- a/apps/mobile/src/features/agent-awareness/notificationPermissions.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { beforeEach, vi } from "vite-plus/test"; -import { describe, expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as Notifications from "expo-notifications"; -import { requestAgentNotificationPermission } from "./notificationPermissions"; - -const platform = vi.hoisted(() => ({ OS: "android" })); -vi.mock("react-native", () => ({ Platform: platform })); -vi.mock("expo-notifications", () => ({ - AndroidImportance: { HIGH: 4 }, - setNotificationChannelAsync: vi.fn(() => Promise.resolve(null)), - getPermissionsAsync: vi.fn(() => Promise.resolve({ granted: false, canAskAgain: true })), - requestPermissionsAsync: vi.fn(() => Promise.resolve({ granted: true, canAskAgain: true })), -})); - -describe("agent notification permission", () => { - beforeEach(() => { - vi.clearAllMocks(); - platform.OS = "android"; - }); - - it.effect("preserves denied permission when Android cannot ask again", () => - Effect.gen(function* () { - vi.mocked(Notifications.getPermissionsAsync).mockResolvedValueOnce({ - granted: false, - canAskAgain: false, - } as Notifications.NotificationPermissionsStatus); - expect(yield* requestAgentNotificationPermission).toEqual({ - type: "denied", - canAskAgain: false, - }); - expect(Notifications.requestPermissionsAsync).not.toHaveBeenCalled(); - }), - ); -}); diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts index 355dbc72e3d26..8886f0bf2720b 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts @@ -598,21 +598,6 @@ describe("makeRelayDeviceRegistrationRequest", () => { }).pipe(Effect.provide(layerRelayTest)); }); - it.effect("re-registers when the stored account identity differs", () => { - Constants.expoConfig!.extra = { - relay: { - url: "https://relay.example.test/", - }, - }; - registrationRecordStore.current = { identity: "someone-else", signature: "stale" }; - setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); - - return Effect.gen(function* () { - yield* refreshAgentAwarenessRegistration(); - expect(saveAgentAwarenessRegistrationRecord).toHaveBeenCalledTimes(1); - }).pipe(Effect.provide(layerRelayTest)); - }); - it.effect("continues queued device registration after a failed auth lookup", () => { Constants.expoConfig!.extra = { relay: { diff --git a/apps/mobile/src/features/connection/environmentSections.test.ts b/apps/mobile/src/features/connection/environmentSections.test.ts index 9320ce2747610..eed95771b1a99 100644 --- a/apps/mobile/src/features/connection/environmentSections.test.ts +++ b/apps/mobile/src/features/connection/environmentSections.test.ts @@ -79,23 +79,6 @@ describe("mobile environment settings sections", () => { expect(sections.availableCloudEnvironments).toEqual([]); }); - it("does not duplicate a saved relay environment in the available cloud listing", () => { - const cloud = connectedEnvironment({ - environmentId: "environment-cloud", - isRelayManaged: true, - connectionState: "available", - }); - const listedCloud = cloudEnvironment("environment-cloud"); - - const sections = splitEnvironmentSections({ - connectedEnvironments: [cloud], - cloudEnvironments: [listedCloud], - }); - - expect(sections.connectedCloudEnvironments).toEqual([cloud]); - expect(sections.availableCloudEnvironments).toEqual([]); - }); - it("still offers a cloud environment saved directly as a local backend", () => { const local = connectedEnvironment({ environmentId: "environment-cloud", diff --git a/apps/mobile/src/features/diagnostics/crash-log-model.test.ts b/apps/mobile/src/features/diagnostics/crash-log-model.test.ts index c3171aa7d77b9..018ebb96932af 100644 --- a/apps/mobile/src/features/diagnostics/crash-log-model.test.ts +++ b/apps/mobile/src/features/diagnostics/crash-log-model.test.ts @@ -44,12 +44,4 @@ describe("parseStartupCrashRecords", () => { ]); expect(records.map((record) => record.timestamp)).toEqual([20, 10]); }); - - it("skips a runtime error entry without a description line", () => { - expect( - parseStartupCrashRecords([ - { ...FATAL, message: "ErrorRecovery fatal exception: Fatal exception: Name: x" }, - ]), - ).toEqual([]); - }); }); diff --git a/apps/mobile/src/features/home/useThreadListActions.test.ts b/apps/mobile/src/features/home/useThreadListActions.test.ts index 2e74d852c5b8d..9a59367a9543a 100644 --- a/apps/mobile/src/features/home/useThreadListActions.test.ts +++ b/apps/mobile/src/features/home/useThreadListActions.test.ts @@ -219,24 +219,6 @@ describe("thread list operation permissions", () => { expect(state.requests).toEqual([]); }); - it.each(mutationCases)( - "%s works when the target environment gains only task permission", - async (handler, action) => { - state.scopes.get(primaryEnvironmentId)!.clear(); - const actions = useThreadListActions(); - state.scopes.get(otherEnvironmentId)!.add(AuthOrchestrationOperateScope); - - await actions[handler]( - makeThread({ environmentId: otherEnvironmentId }), - "2099-01-01T00:00:00.000Z", - ); - - expect(state.requests).toEqual([ - expect.objectContaining({ action, environmentId: otherEnvironmentId }), - ]); - }, - ); - it("rechecks permission after a retained delete confirmation", async () => { useThreadListActions().confirmDeleteThread(makeThread()); const confirm = state.dialogs[0]?.onConfirm; @@ -368,6 +350,7 @@ describe("thread action undo", () => { expect(readThreadUndoNotice()).toBeNull(); }); + // regression: #21401 restore archive and thread action undo it("wakes a snoozed thread through the undo action", async () => { const thread = makeThread(); await useThreadListActions().snoozeThread(thread, "2099-01-01T09:00:00.000Z"); @@ -410,6 +393,7 @@ describe("thread action undo", () => { }, ); + // regression: #21401 restore archive and thread action undo it("does not restore snooze if undoing an unpin fails", async () => { const thread = makeThread({ pinnedAt: "2026-09-02T00:00:00.000Z", @@ -431,6 +415,7 @@ describe("thread action undo", () => { expect(readThreadUndoNotice()).toBeNull(); }); + // regression: #21401 restore archive and thread action undo it("does not offer undo for a failed action", async () => { state.failedAction = "settle"; expect(await useThreadListActions().settleThread(makeThread())).toBe(false); @@ -456,6 +441,7 @@ describe("thread action undo", () => { expect(state.requests.map(({ action }) => action)).toEqual(["unpin"]); }); + // regression: #21401 restore archive and thread action undo it("expires undo after five seconds", async () => { vi.useFakeTimers(); await useThreadListActions().unpinThread(makeThread()); @@ -466,6 +452,7 @@ describe("thread action undo", () => { expect(state.requests.map(({ action }) => action)).toEqual(["unpin"]); }); + // regression: #21401 restore archive and thread action undo it("refreshes the archive after archiving on a settlement-capable server", async () => { await useThreadListActions().archiveThread(makeThread()); expect(state.requests.map(({ action }) => action)).toEqual(["archive"]); diff --git a/apps/mobile/src/features/home/workspace-connection-status.test.ts b/apps/mobile/src/features/home/workspace-connection-status.test.ts index dec5899a099bf..abef14fd427fd 100644 --- a/apps/mobile/src/features/home/workspace-connection-status.test.ts +++ b/apps/mobile/src/features/home/workspace-connection-status.test.ts @@ -23,9 +23,6 @@ function workspaceState(overrides: Partial = {}): WorkspaceState describe("workspace connection status", () => { it.each([ - ["connecting", null], - ["reconnecting", null], - ["connecting", "Could not reach sleeping laptop"], ["reconnecting", "Could not reach sleeping laptop"], ["error", "Could not reach sleeping laptop"], ] as const)( diff --git a/apps/mobile/src/features/projects/AddProjectScreen.test.ts b/apps/mobile/src/features/projects/AddProjectScreen.test.ts index 893abc1473453..fe2cb546a3ca2 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.test.ts +++ b/apps/mobile/src/features/projects/AddProjectScreen.test.ts @@ -10,10 +10,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test" const state = vi.hoisted(() => ({ scopes: new Set(), - otherScopes: new Set(), baseDirectory: "", projects: [] as string[], - createRequests: [] as { environmentId: string; workspaceRoot: string }[], })); vi.mock("react", async (importOriginal) => ({ @@ -54,10 +52,8 @@ vi.mock("../../components/ui/ErrorBanner", () => ({ ErrorBanner: "ErrorBanner" } vi.mock("./SourceControlIcon", () => ({ SourceControlIcon: "SourceControlIcon" })); vi.mock("../../lib/uuid", () => ({ uuidv4: () => "project" })); vi.mock("../../state/session", () => ({ - useEnvironmentScope: (environmentId: string, scope: string) => - (environmentId === "environment" ? state.scopes : state.otherScopes).has(scope), - readEnvironmentScope: (environmentId: string, scope: string) => - (environmentId === "environment" ? state.scopes : state.otherScopes).has(scope), + useEnvironmentScope: (_environmentId: string, scope: string) => state.scopes.has(scope), + readEnvironmentScope: (_environmentId: string, scope: string) => state.scopes.has(scope), })); vi.mock("../../state/entities", () => ({ useProjects: () => [], @@ -70,27 +66,16 @@ vi.mock("../../state/entities", () => ({ settings: { addProjectBaseDirectory: state.baseDirectory }, }, ], - [ - "other-environment", - { - environment: { platform: { os: "linux" }, capabilities: {} }, - settings: { addProjectBaseDirectory: state.baseDirectory }, - }, - ], ]), })); vi.mock("../../state/use-remote-environment-registry", () => ({ useRemoteEnvironmentRuntime: () => ({ connectionState: "connected" }), useRemoteConnectionStatus: () => ({ - connectedEnvironments: [ - { environmentId: "environment", connectionState: "connected" }, - { environmentId: "other-environment", connectionState: "connected" }, - ], + connectedEnvironments: [{ environmentId: "environment", connectionState: "connected" }], }), useSavedRemoteConnections: () => ({ savedConnectionsById: { connection: { environmentId: "environment", environmentLabel: "Environment" }, - other: { environmentId: "other-environment", environmentLabel: "Other environment" }, }, }), })); @@ -111,16 +96,8 @@ vi.mock("../../state/sourceControl", () => ({ })); vi.mock("../../state/projects", () => ({ projectEnvironment: { - create: async ({ - environmentId, - input, - }: { - environmentId: string; - input: { workspaceRoot: string }; - }) => { - state.createRequests.push({ environmentId, workspaceRoot: input.workspaceRoot }); - const scopes = environmentId === "environment" ? state.scopes : state.otherScopes; - if (!scopes.has(AuthOrchestrationOperateScope)) { + create: async ({ input }: { input: { workspaceRoot: string } }) => { + if (!state.scopes.has(AuthOrchestrationOperateScope)) { return AsyncResult.failure(Cause.fail(new Error("Project creation denied"))); } state.projects.push(input.workspaceRoot); @@ -129,7 +106,7 @@ vi.mock("../../state/projects", () => ({ }, })); -import { AddProjectDestinationScreen, AddProjectLocalFolderScreen } from "./AddProjectScreen"; +import { AddProjectDestinationScreen } from "./AddProjectScreen"; function findAction(node: ReactNode, label: string): (() => unknown) | null { if (Array.isArray(node)) { @@ -159,21 +136,13 @@ function cloneAction() { return action; } -function localProjectAction(environmentId = "environment") { - const action = findAction(AddProjectLocalFolderScreen({ environmentId }), "Add project"); - if (!action) throw new Error("Add project action missing"); - return action; -} - describe("clone project permissions", () => { beforeEach(async () => { state.baseDirectory = await NodeFSP.mkdtemp( NodePath.join(NodeOS.tmpdir(), "t3-clone-permissions-"), ); state.scopes = new Set([AuthSourceControlWriteScope]); - state.otherScopes = new Set(); state.projects = []; - state.createRequests = []; }); afterEach(async () => { @@ -200,56 +169,3 @@ describe("clone project permissions", () => { }, ); }); - -describe("local project permissions", () => { - beforeEach(() => { - state.baseDirectory = "/workspace/project"; - state.scopes = new Set(); - state.otherScopes = new Set(); - state.projects = []; - state.createRequests = []; - }); - - it.each([false, true])( - "does not dispatch project creation without the current grant (revoked: %s)", - async (revokeBeforeSubmit) => { - if (revokeBeforeSubmit) state.scopes.add(AuthOrchestrationOperateScope); - const submit = localProjectAction(); - state.scopes.delete(AuthOrchestrationOperateScope); - - await submit(); - - expect(state.createRequests).toEqual([]); - expect(state.projects).toEqual([]); - }, - ); - - it("adds a typed local path with only task-operation permission", async () => { - state.scopes.add(AuthOrchestrationOperateScope); - - await localProjectAction()(); - - expect(state.createRequests).toEqual([ - { environmentId: "environment", workspaceRoot: state.baseDirectory }, - ]); - expect(state.projects).toEqual([state.baseDirectory]); - }); - - it.each([false, true])( - "uses the selected environment's grant (allowed: %s)", - async (allowSelectedEnvironment) => { - (allowSelectedEnvironment ? state.otherScopes : state.scopes).add( - AuthOrchestrationOperateScope, - ); - - await localProjectAction("other-environment")(); - - expect(state.createRequests).toEqual( - allowSelectedEnvironment - ? [{ environmentId: "other-environment", workspaceRoot: state.baseDirectory }] - : [], - ); - expect(state.projects).toEqual(allowSelectedEnvironment ? [state.baseDirectory] : []); - }, - ); -}); diff --git a/apps/mobile/src/features/prompt-links/prompt-link-model.test.ts b/apps/mobile/src/features/prompt-links/prompt-link-model.test.ts index 8c6d6a6c17649..9bf74ef042ddc 100644 --- a/apps/mobile/src/features/prompt-links/prompt-link-model.test.ts +++ b/apps/mobile/src/features/prompt-links/prompt-link-model.test.ts @@ -1,7 +1,7 @@ import { parseNewThreadLink } from "@t3tools/shared/promptLinks"; import { describe, expect, it } from "vite-plus/test"; -import { linkedThreadDraftAction, promptLinkSearchFromParams } from "./prompt-link-model"; +import { promptLinkSearchFromParams } from "./prompt-link-model"; describe("promptLinkSearchFromParams", () => { it("round-trips decoded route params into the shared parser", () => { @@ -24,11 +24,3 @@ describe("promptLinkSearchFromParams", () => { }); }); }); - -describe("linkedThreadDraftAction", () => { - it("fills an empty composer and asks before replacing typed text", () => { - expect(linkedThreadDraftAction(" \n", "Do it")).toBe("fill"); - expect(linkedThreadDraftAction("Do it", "Do it")).toBe("keep"); - expect(linkedThreadDraftAction("My notes", "Do it")).toBe("ask"); - }); -}); diff --git a/apps/mobile/src/features/pull-requests/guide/reviewGuideState.test.ts b/apps/mobile/src/features/pull-requests/guide/reviewGuideState.test.ts deleted file mode 100644 index bde0b3e6a4e28..0000000000000 --- a/apps/mobile/src/features/pull-requests/guide/reviewGuideState.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; -import { - IsoDateTime, - ProviderInstanceId, - type PullRequestReviewGuideResult, -} from "@t3tools/contracts"; -import { reviewGuideState } from "./reviewGuideState"; - -const guide: NonNullable = { - headSha: "old-head", - generatedAt: IsoDateTime.make("2026-10-11T00:00:00.000Z"), - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" }, - stops: [], -}; - -describe("mobile review guide state", () => { - it("keeps a previous guide readable during a rewrite", () => { - const state = reviewGuideState({ - guide, - currentHeadSha: "new-head", - run: { _tag: "Running", headSha: "new-head", startedAt: guide.generatedAt }, - }); - expect(state).toMatchObject({ guide, stale: true, running: true, failure: null }); - }); -}); diff --git a/apps/mobile/src/features/pull-requests/pullRequestList.test.ts b/apps/mobile/src/features/pull-requests/pullRequestList.test.ts index 32ee4aef0b035..06733a9cf3648 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestList.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestList.test.ts @@ -66,25 +66,6 @@ describe("recycled PR list rows", () => { environmentId: EnvironmentId.make("env-one"), }); - it.each([ - { title: "New title" }, - { additions: 8 }, - { deletions: 4 }, - { checksState: "failing" as const }, - { reviewDecision: "approved" as const }, - { viewerReviewRequested: true }, - { isDraft: true }, - { state: "closed" as const }, - { mergeability: "conflicting" as const }, - { baseBranch: "release" }, - { author: { login: "another", name: null, avatarUrl: null } }, - { author: { login: "octocat", name: null, avatarUrl: "https://example.com/avatar.png" } }, - { labels: [{ name: "bug", color: "ff0000" }] }, - { stack: { number: 1, position: 2, size: 3, base: "main" } }, - ])("updates the row when visible status or quick actions change: %j", (patch) => { - expect(pullRequestListRowsAreEqual(scopedEntry(), scopedEntry(patch))).toBe(false); - }); - it("updates navigation and commands when a duplicate moves to another environment", () => { const original = scopedEntry(); expect( diff --git a/apps/mobile/src/features/pull-requests/pullRequestReviewDrafts.test.ts b/apps/mobile/src/features/pull-requests/pullRequestReviewDrafts.test.ts index cf59ccc9e9a17..fb659f5b1502f 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestReviewDrafts.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestReviewDrafts.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import { - clearSubmittedReview, - pullRequestReviewKey, - type ReviewDraft, -} from "./pullRequestReviewDrafts"; +import { clearSubmittedReview, type ReviewDraft } from "./pullRequestReviewDrafts"; const comment = { id: "one", path: "renamed.ts", @@ -35,23 +31,4 @@ describe("mobile review draft recovery", () => { comments: [], }); }); - it("does not mix identical PR numbers across repositories, hosts, or environments", () => { - const ref = { - environmentId: "a", - projectId: "p", - host: "github.com", - repository: "one/repo", - number: 1, - }; - expect( - new Set( - [ - ref, - { ...ref, environmentId: "b" }, - { ...ref, host: "github.enterprise" }, - { ...ref, repository: "two/repo" }, - ].map(pullRequestReviewKey), - ).size, - ).toBe(4); - }); }); diff --git a/apps/mobile/src/features/pull-requests/usePullRequestAgentActions.test.ts b/apps/mobile/src/features/pull-requests/usePullRequestAgentActions.test.ts deleted file mode 100644 index 9fe12ec259b45..0000000000000 --- a/apps/mobile/src/features/pull-requests/usePullRequestAgentActions.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; -import type { PullRequestDetail } from "@t3tools/contracts"; -import { usePullRequestAgentActions } from "./usePullRequestAgentActions"; - -const state = vi.hoisted(() => ({ - connected: false, - canAsk: true, - ask: vi.fn(), -})); -vi.mock("./usePullRequestConnected", () => ({ - usePullRequestConnected: () => state.connected, -})); -vi.mock("./usePullRequestThreads", () => ({ - usePullRequestThreads: () => ({ - threads: [], - canAsk: state.canAsk, - canWatch: true, - busy: false, - isPending: true, - ask: state.ask, - }), -})); -const params = { - environmentId: "environment", - projectId: "project", - repository: "owner/repo", - number: 1, -}; -const pr = { state: "open", mergeability: "conflicting" } as PullRequestDetail; - -beforeEach(() => { - state.connected = false; - state.canAsk = true; - state.ask.mockClear(); -}); - -describe("PR agent drafts", () => { - it("does not open drafts when the connection denies task access", () => { - state.connected = true; - state.canAsk = false; - usePullRequestAgentActions(params, pr).start("explain"); - expect(state.ask).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/mobile/src/features/review/reviewModel.test.ts b/apps/mobile/src/features/review/reviewModel.test.ts index f472a627286f4..c4f64329779fc 100644 --- a/apps/mobile/src/features/review/reviewModel.test.ts +++ b/apps/mobile/src/features/review/reviewModel.test.ts @@ -5,7 +5,6 @@ import { applyReviewDiffMetadata, buildReviewParsedDiff } from "./reviewModel"; describe("buildReviewParsedDiff", () => { it.each([ ["a/example.ts", "a/example.ts"], - ["b/example.ts", "b/example.ts"], ["a/before.ts", "b/after.ts"], ])("preserves repository paths from %s to %s", (previousPath, path) => { const parsed = buildReviewParsedDiff( diff --git a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx index 0f4dd8ae23be5..d01ba41b7e3c3 100644 --- a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx +++ b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx @@ -89,6 +89,7 @@ describe("BranchNamingSettings", () => { }); }); + // regression: #18381 branch naming text saves when you leave the screen it("saves both drafts in one patch when the screen closes", () => { render("static"); act(() => input(PREFIX).onChangeText("feat/")); diff --git a/apps/mobile/src/features/settings/lib/legal-document-url.test.ts b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts index 340aa22148bfa..001c1fbc299cd 100644 --- a/apps/mobile/src/features/settings/lib/legal-document-url.test.ts +++ b/apps/mobile/src/features/settings/lib/legal-document-url.test.ts @@ -3,12 +3,10 @@ import { describe, expect, it } from "vite-plus/test"; import { isLegalDocumentUrl } from "./legal-document-url"; describe("isLegalDocumentUrl", () => { - it.each([ - "https://t3.codes/download", - "https://example.com/legal", - "javascript:alert(1)", - "not-a-url", - ])("rejects a URL outside the legal-document allowlist: %s", (url) => { - expect(isLegalDocumentUrl(url)).toBe(false); - }); + it.each(["https://example.com/legal", "javascript:alert(1)"])( + "rejects a URL outside the legal-document allowlist: %s", + (url) => { + expect(isLegalDocumentUrl(url)).toBe(false); + }, + ); }); diff --git a/apps/mobile/src/features/settings/scheduledTaskDraft.test.ts b/apps/mobile/src/features/settings/scheduledTaskDraft.test.ts index 6da0de6ab07e9..8cf8bb5b2b72d 100644 --- a/apps/mobile/src/features/settings/scheduledTaskDraft.test.ts +++ b/apps/mobile/src/features/settings/scheduledTaskDraft.test.ts @@ -5,12 +5,9 @@ import { ProviderDriverKind, ProviderInstanceId, ProjectId, - ScheduledTaskId, - type ScheduledTask, } from "@t3tools/contracts"; import { scheduledTaskDefaultModel, - editDraft, DEFAULT_SCHEDULE, scheduleDraftForTask, scheduleFromDraft, @@ -92,10 +89,7 @@ describe("scheduleFromDraft", () => { it.each([ ["1.5", 90_000], - ["1000001", 60_000_060_000], [String(60_001 / 60_000), 60_001], - [String(65_000 / 60_000), 65_000], - [String(123_456 / 60_000), 123_456], ])("preserves a valid %s minute interval when saving", (intervalMinutes, everyMs) => { expect(scheduleFromDraft({ ...DEFAULT_SCHEDULE, mode: "interval", intervalMinutes })).toEqual({ type: "interval", @@ -104,37 +98,6 @@ describe("scheduleFromDraft", () => { }); }); -const legacyTask: ScheduledTask = { - id: ScheduledTaskId.make("legacy-task"), - title: "Review issues", - prompt: "Review open issues", - enabled: true, - schedule: { type: "interval", everyMs: 60_000 }, - projectId: ProjectId.make("project"), - threadId: null, - workspaceStrategy: { type: "worktree", baseRef: "release" }, - modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, - runtimeMode: "full-access", - interactionMode: "default", - createdBy: "user", - creationSource: "web", - createdAt: "2026-09-17T00:00:00.000Z", - updatedAt: "2026-09-17T00:00:00.000Z", - nextRunAt: null, - lastRunAt: null, - lastRunStatus: "never", - lastRunError: null, - runCount: 0, -}; - -describe("editing scheduled task branch settings", () => { - it("keeps an omitted origin flag on the local base branch", () => { - const draft = editDraft(legacyTask); - expect(draft.baseRef).toBe("release"); - expect(draft.startFromOrigin).toBe(false); - }); -}); - describe("scheduled task model defaults", () => { const instanceId = ProviderInstanceId.make("codex"); const projectId = ProjectId.make("project"); diff --git a/apps/mobile/src/features/sharing/incoming-share-model.test.ts b/apps/mobile/src/features/sharing/incoming-share-model.test.ts index 588345fdf7333..f9992867141f6 100644 --- a/apps/mobile/src/features/sharing/incoming-share-model.test.ts +++ b/apps/mobile/src/features/sharing/incoming-share-model.test.ts @@ -253,32 +253,6 @@ describe("incoming native shares", () => { ]); }); - it("records the stored copy's measured size when a content URI under-reports", async () => { - const file: SharePayload = { - shareType: "file", - value: "content://shared/report", - mimeType: "application/pdf", - }; - const persistFile = vi.fn(async () => "file:///documents/report.pdf"); - const readSize = vi.fn(async (uri: string) => (uri.startsWith("content:") ? 42 : 4200)); - - const result = await buildIncomingShareDraft({ - id: "share-android-report", - createdAt: "2026-07-15T10:00:00.000Z", - payloads: [file], - resolvedPayloads: [], - fileReader: { - readBase64: async () => "unused", - persistFile, - readSize, - removeOwnedFile: async () => undefined, - }, - }); - - expect(result.attachments).toHaveLength(1); - expect(result.attachments[0]?.sizeBytes).toBe(4200); - }); - it("rejects a shared file whose persisted copy measures empty and releases the copy", async () => { const file: SharePayload = { shareType: "file", @@ -360,36 +334,6 @@ describe("incoming native shares", () => { expect(removeOwnedFile.mock.calls.map(([uri]) => uri)).toEqual([resolvedUri, payloadUri]); }); - it("keeps images and rejects shared files on servers without file support", () => { - const image = { - id: "image-1", - type: "image" as const, - name: "image.png", - mimeType: "image/png", - sizeBytes: 3, - dataUrl: "data:image/png;base64,YWJj", - previewUri: "data:image/png;base64,YWJj", - }; - const file = { - id: "file-1", - type: "file" as const, - name: "report.pdf", - mimeType: "application/pdf", - sizeBytes: 42, - fileUri: "file:///documents/report.pdf", - }; - - expect( - selectIncomingShareAttachments({ - attachments: [image, file], - maxFileAttachmentBytes: null, - }), - ).toEqual({ - attachments: [image], - warnings: ["'report.pdf' was skipped because this server does not support files."], - }); - }); - it("uses current server support and limits when selecting a reserved share", () => { const file = { id: "file-1", @@ -574,15 +518,6 @@ describe("share cleanup ownership", () => { "file:///var/mobile/Containers/Shared/AppGroup/GROUP", ]; - it("allows deleting files inside the app's own directories", () => { - expect( - isShareFileUriUnderOwnedRoots( - "file:///var/mobile/Containers/Shared/AppGroup/GROUP/shared.pdf", - ownedRoots, - ), - ).toBe(true); - }); - it("treats /private/var and /var as the same iOS location", () => { expect( isShareFileUriUnderOwnedRoots( diff --git a/apps/mobile/src/features/sharing/incoming-share-native.test.ts b/apps/mobile/src/features/sharing/incoming-share-native.test.ts index e5f0587157b7f..1f674618c5461 100644 --- a/apps/mobile/src/features/sharing/incoming-share-native.test.ts +++ b/apps/mobile/src/features/sharing/incoming-share-native.test.ts @@ -17,6 +17,7 @@ describe("createIncomingSharePayloadReader", () => { expect(readPayloads).toHaveBeenCalledTimes(1); }); + // regression: #4943 stop shared content errors in Personal Team builds it("preserves other native errors", () => { const error = new Error("native failure"); const read = createIncomingSharePayloadReader({ diff --git a/apps/mobile/src/features/terminal/terminalLaunchContext.test.ts b/apps/mobile/src/features/terminal/terminalLaunchContext.test.ts index 309aaa61319c8..89d3edc7fa303 100644 --- a/apps/mobile/src/features/terminal/terminalLaunchContext.test.ts +++ b/apps/mobile/src/features/terminal/terminalLaunchContext.test.ts @@ -81,34 +81,4 @@ describe("pending terminal launches", () => { }); expect(takePendingTerminalLaunch(target)).toBeNull(); }); - - it("keeps pending launches isolated per terminal target", () => { - const primaryTarget = { - environmentId: EnvironmentId.make("env-1"), - threadId: ThreadId.make("thread-1"), - terminalId: "term-2", - }; - const otherTarget = { - environmentId: EnvironmentId.make("env-1"), - threadId: ThreadId.make("thread-1"), - terminalId: "term-3", - }; - - stagePendingTerminalLaunch({ - target: primaryTarget, - launch: { - cwd: "/repo/root", - worktreePath: null, - initialInput: "pnpm i\r", - }, - }); - - expect(takePendingTerminalLaunch(otherTarget)).toBeNull(); - expect(takePendingTerminalLaunch(primaryTarget)).toEqual({ - cwd: "/repo/root", - worktreePath: null, - env: undefined, - initialInput: "pnpm i\r", - }); - }); }); diff --git a/apps/mobile/src/features/terminal/useTerminalSurfaceBuffer.test.ts b/apps/mobile/src/features/terminal/useTerminalSurfaceBuffer.test.ts index 2a1cf2a048481..bd23efce94e14 100644 --- a/apps/mobile/src/features/terminal/useTerminalSurfaceBuffer.test.ts +++ b/apps/mobile/src/features/terminal/useTerminalSurfaceBuffer.test.ts @@ -58,14 +58,13 @@ afterEach(async () => { vi.unstubAllGlobals(); }); -it.each([ - "secondary:thread:term-1", - "environment:another-thread:term-1", - "environment:thread:term-2", -])("does not carry history into another terminal target (%s)", async (terminalKey) => { - await render(); - await render({ terminalKey, buffer: null }); - expect(displayed).toHaveBeenLastCalledWith({ buffer: "", readOnly: false }); - await render({ terminalKey, buffer: "new target" }); - expect(displayed).toHaveBeenLastCalledWith({ buffer: "new target", readOnly: false }); -}); +it.each(["secondary:thread:term-1", "environment:thread:term-2"])( + "does not carry history into another terminal target (%s)", + async (terminalKey) => { + await render(); + await render({ terminalKey, buffer: null }); + expect(displayed).toHaveBeenLastCalledWith({ buffer: "", readOnly: false }); + await render({ terminalKey, buffer: "new target" }); + expect(displayed).toHaveBeenLastCalledWith({ buffer: "new target", readOnly: false }); + }, +); diff --git a/apps/mobile/src/features/threads/checkout-new-task-branch.test.ts b/apps/mobile/src/features/threads/checkout-new-task-branch.test.ts index 2d92c979f94d4..bee63a9775a05 100644 --- a/apps/mobile/src/features/threads/checkout-new-task-branch.test.ts +++ b/apps/mobile/src/features/threads/checkout-new-task-branch.test.ts @@ -326,8 +326,6 @@ describe("new-task branch checkout", () => { it.each([ { source: "historical", nested: true }, - { source: "picker", nested: true }, - { source: "historical", nested: false }, { source: "picker", nested: false }, ])( "rejects $source worktree reuse when project root is unsafe (nested: $nested)", diff --git a/apps/mobile/src/features/threads/composerSendPresentation.test.ts b/apps/mobile/src/features/threads/composerSendPresentation.test.ts index 50b3590408dac..85f45baee8e58 100644 --- a/apps/mobile/src/features/threads/composerSendPresentation.test.ts +++ b/apps/mobile/src/features/threads/composerSendPresentation.test.ts @@ -11,31 +11,6 @@ const idle = { } as const; describe("resolveComposerSendPresentation", () => { - it("follows the configured behavior once a turn is running", () => { - const queueing = resolveComposerSendPresentation({ - ...idle, - running: true, - canSteer: true, - followUpBehavior: "queue", - }); - const steering = resolveComposerSendPresentation({ - ...idle, - running: true, - canSteer: true, - followUpBehavior: "steer", - }); - - expect(queueing.label).toBe("Queue"); - expect(queueing.icon).toBe("list.number"); - expect(queueing.action).toBe("queue"); - expect(queueing.alternate).toBe("steer"); - expect(steering.label).toBe("Steer"); - expect(steering.icon).toBe("arrow.turn.left.up"); - expect(steering.action).toBe("steer"); - expect(steering.alternate).toBe("queue"); - expect(steering.offersFollowUpChoice).toBe(true); - }); - it("never promises steering the provider cannot do", () => { const presentation = resolveComposerSendPresentation({ ...idle, diff --git a/apps/mobile/src/features/threads/new-task-context-presentation.test.ts b/apps/mobile/src/features/threads/new-task-context-presentation.test.ts index 2e225202cc561..64360ff905e7e 100644 --- a/apps/mobile/src/features/threads/new-task-context-presentation.test.ts +++ b/apps/mobile/src/features/threads/new-task-context-presentation.test.ts @@ -34,6 +34,7 @@ describe("resolveNewTaskWorkspaceSelection", () => { }, ); + // regression: #21404 use current checkout for non-git projects it("preserves the selection and defaults for git projects", () => { const selection = { mode: "local", branch: "feature", worktreePath: "/worktree" } as const; expect(resolveNewTaskWorkspaceSelection({ selection, fallback, isGitRepo: true })).toBe( diff --git a/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts b/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts index d4ec7dacba53e..b8c067ca826a2 100644 --- a/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts +++ b/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts @@ -63,11 +63,7 @@ describe("tool-group append following", () => { it.each([ { name: "reading earlier calls", changes: { scrollOffset: 20 } }, - { name: "dragging before leaving the edge", changes: { userScrolling: true } }, - { name: "opening detail during an append", changes: { detailsChanged: true } }, { name: "streaming a result", changes: { rows: previousRows, contentHeight: 600 } }, - { name: "updating a lifecycle label", changes: { rows: previousRows, contentHeight: 289 } }, - { name: "prepending old calls", changes: { rows: [{ id: "older" }, ...previousRows] } }, { name: "replacing a call while appending", changes: { rows: [{ id: "replacement" }, ...appendedRows.slice(1)] }, @@ -114,6 +110,7 @@ describe("resolveThreadFeedSubmissionAnchor", () => { ).toBeNull(); }); + // regression: #7897 stop pushing follow-up messages to the top it("does not anchor a thread that has already started a turn", () => { expect( resolveThreadFeedSubmissionAnchor({ @@ -128,6 +125,7 @@ describe("resolveThreadFeedSubmissionAnchor", () => { }); describe("resolveThreadFeedLiveFollow", () => { + // regression: #5986 stabilize thread composer and interactions it("pauses immediately when the user starts scrolling", () => { expect(resolveThreadFeedLiveFollow(true, { type: "user-scroll-begin" })).toBe(false); }); diff --git a/apps/mobile/src/features/threads/thread-settings-options.test.ts b/apps/mobile/src/features/threads/thread-settings-options.test.ts index aef1062feaede..81baead620e0b 100644 --- a/apps/mobile/src/features/threads/thread-settings-options.test.ts +++ b/apps/mobile/src/features/threads/thread-settings-options.test.ts @@ -23,6 +23,7 @@ const effortDescriptor: Extract = }; describe("selectableChoices", () => { + // regression: #21406 bring model controls in line with web it("includes prompt-injected and workflow choices in declared order", () => { expect(selectableChoices(effortDescriptor).map((choice) => choice.id)).toEqual([ "low", diff --git a/apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts b/apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts index 55256e08d3e9f..53b5874c7e3a7 100644 --- a/apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts +++ b/apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts @@ -133,6 +133,7 @@ describe("thread settings sheet state", () => { ]); }); + // regression: #16816 sync model favorites and visibility across clients it("cannot select a hidden model exposed only for visibility management", () => { const pending = modelOption("gpt-next"); expect( diff --git a/apps/mobile/src/features/threads/threadContentPresentation.test.ts b/apps/mobile/src/features/threads/threadContentPresentation.test.ts index 8167553ed9519..3990799d85925 100644 --- a/apps/mobile/src/features/threads/threadContentPresentation.test.ts +++ b/apps/mobile/src/features/threads/threadContentPresentation.test.ts @@ -28,19 +28,4 @@ describe("thread content presentation", () => { detail: "Reconnect this environment to load the conversation.", }); }); - - it("surfaces detail errors before presenting a loading state", () => { - expect( - projectThreadContentPresentation({ - hasDetail: false, - detailError: "The thread stream failed.", - detailDeleted: false, - connectionState: "connected", - }), - ).toEqual({ - kind: "unavailable", - title: "Could not load conversation", - detail: "The thread stream failed.", - }); - }); }); diff --git a/apps/mobile/src/features/threads/threadDragGap.test.ts b/apps/mobile/src/features/threads/threadDragGap.test.ts deleted file mode 100644 index fe3704a798878..0000000000000 --- a/apps/mobile/src/features/threads/threadDragGap.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; -import { threadDragAction, threadOrderAfterMove } from "./threadOrder"; - -describe("drag action labels", () => { - it("does not offer a parked-section reorder or snooze without a wake time", () => { - expect(threadDragAction("settled", "settled")).toBeNull(); - expect(threadDragAction("active", "snoozed")).toBeNull(); - expect( - threadOrderAfterMove(["a", "b"], "a", { - section: "settled", - targetId: null, - placement: "before", - }), - ).toBeNull(); - }); -}); diff --git a/apps/mobile/src/features/threads/threadListV2.test.ts b/apps/mobile/src/features/threads/threadListV2.test.ts index 2621d4551865c..a67e3a6fc8a91 100644 --- a/apps/mobile/src/features/threads/threadListV2.test.ts +++ b/apps/mobile/src/features/threads/threadListV2.test.ts @@ -12,7 +12,6 @@ import { type ThreadMoveAvailability, } from "./threadOrder"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { resolveSnoozePresets } from "@t3tools/client-runtime/state/thread-settled"; import { CommandId, @@ -36,7 +35,6 @@ import { resolveThreadListV2SnoozeMenuSelection, resolveThreadListV2Status, resolveThreadListV2WokeAt, - resolveThreadListV2SwipeActions, sortThreadsForListV2, threadListV2ListItemsAreEqual, threadHasUnseenCompletion, @@ -151,32 +149,6 @@ describe("queued messages keep a settled thread active", () => { ]); expect(layout.settledCount).toBe(1); }); - - it("includes it in the reorderable active section", () => { - expect( - getThreadListV2OrderedSection({ threads, section: "active", now: NOW, queuedThreadKeys }).map( - (thread) => thread.id, - ), - ).toEqual(["active", "settled-queued"]); - expect( - getThreadListV2OrderedSection({ threads, section: "active", now: NOW }).map( - (thread) => thread.id, - ), - ).toEqual(["active"]); - }); -}); - -describe("resolveThreadListV2SwipeActions", () => { - it("falls back to archive only for a pre-lifecycle server", () => { - expect( - resolveThreadListV2SwipeActions({ - variant: "card", - settlementSupported: false, - snoozeSupported: false, - snoozable: true, - }), - ).toEqual({ primary: "archive", secondary: null }); - }); }); describe("sortThreadsForListV2", () => { @@ -433,34 +405,6 @@ describe("buildThreadListV2Items", () => { expect(layout.settledShelfHeaderIndex).toBe(1); }); - it("keeps the selected settled thread visible when its shelf is collapsed", () => { - const layout = buildThreadListV2Items({ - threads: [ - makeThread({ - id: ThreadId.make("selected"), - title: "Selected", - settledOverride: "settled", - settledAt: NOW, - }), - makeThread({ - id: ThreadId.make("other"), - title: "Other", - settledOverride: "settled", - settledAt: NOW, - }), - ], - environmentId: null, - searchQuery: "", - now: NOW, - settledShelfExpanded: false, - selectedThreadKey: `${environmentId}:selected`, - }); - - expect(layout.items.map((item) => item.thread.id)).toEqual(["selected"]); - expect(layout.settledCount).toBe(2); - expect(layout.settledShelfHeaderIndex).toBe(0); - }); - it("keeps settled threads in the tail and filters by search query", () => { const { items } = buildThreadListV2Items({ threads: [ @@ -484,48 +428,6 @@ describe("buildThreadListV2Items", () => { ]); }); - it("includes a thread matched by message content", () => { - const thread = makeThread({ - id: ThreadId.make("content-match"), - title: "Unrelated title", - }); - const { items } = buildThreadListV2Items({ - threads: [thread], - environmentId: null, - searchQuery: "relay reconnect", - matchedThreadKeys: new Set([ - threadSearchMatchKey({ - environmentId, - threadId: thread.id, - }), - ]), - now: NOW, - }); - - expect(items.map((item) => item.thread.id)).toEqual(["content-match"]); - }); - - it("scopes the flat list to one project", () => { - const projectId = ProjectId.make("project-1"); - const otherProjectId = ProjectId.make("project-2"); - const { items } = buildThreadListV2Items({ - threads: [ - makeThread({ id: ThreadId.make("included"), projectId, title: "Included" }), - makeThread({ - id: ThreadId.make("excluded"), - projectId: otherProjectId, - title: "Excluded", - }), - ], - environmentId: null, - projectRefs: [{ environmentId, projectId }], - searchQuery: "", - now: NOW, - }); - - expect(items.map((item) => item.thread.id)).toEqual(["included"]); - }); - it("scopes the flat list to every environment member of a logical project", () => { const remoteEnvironmentId = EnvironmentId.make("environment-remote"); const projectId = ProjectId.make("project-1"); @@ -898,12 +800,6 @@ describe("pending mobile thread moves", () => { } }); - it("keeps search results in the full pending section order", () => { - const { rows, assignments, pending, update } = fixture(); - const current = update(update(rows, assignments[0]!), assignments[1]!); - expect(layout(current, pending, "match")).toEqual(["c", "b"]); - }); - it("releases for real section membership and foreign key changes", () => { const { rows, pending } = fixture(); expect(reconcilePendingThreadOrder(pending, rows.slice(1))).toBeNull(); @@ -1022,20 +918,6 @@ describe("mobile move availability", () => { expect(assignments![0]!.orderKey > "dd" && assignments![0]!.orderKey < "ff").toBe(true); }, ); - - it("allows an independent keyed move despite an unsupported keyless row elsewhere", () => { - const ordered = rows("active", [null, null, "bb", "dd", "ff"]); - const plan = createThreadMovePlanner({ - ordered, - section: "active", - reorderableEnvironmentIds: new Set([environmentId]), - }); - const assignments = plan(`${environmentId}:move-4`, "up"); - expect(assignments).toHaveLength(1); - expect(assignments![0]!.id).toBe(`${environmentId}:move-4`); - expect(assignments![0]!.orderKey > "bb").toBe(true); - expect(assignments![0]!.orderKey < "dd").toBe(true); - }); }); describe("thread drag destinations", () => { @@ -1214,15 +1096,6 @@ describe("cross-section thread drops", () => { unsnooze: false, }); }); - it("does not send lifecycle commands for an ordinary Active reorder", () => { - expect( - threadDropLifecycle( - makeThread({ id: ThreadId.make("active"), title: "active" }), - "active", - NOW, - ), - ).toEqual({ pin: false, unpin: false, unsettle: false, unsnooze: false }); - }); }); it("excludes subagents from navigation, search and ordering while retaining user forks", () => { @@ -1332,24 +1205,6 @@ describe("threadListV2ListItemsAreEqual", () => { } }); - it("notices a replaced thread shell", () => { - const replacement = makeThread({ id: ThreadId.make("eq"), title: "renamed" }); - const rebuilt = buildThreadListV2Items({ - threads: [replacement], - environmentId: null, - searchQuery: "", - now: NOW, - }); - const next = buildThreadListV2ListItems({ - items: rebuilt.items, - pendingTasks: [], - snoozeLabelNow: NOW, - }); - const previousThread = build().find((item) => item.type === "v2-thread")!; - const nextThread = next.find((item) => item.type === "v2-thread")!; - expect(threadListV2ListItemsAreEqual(previousThread, nextThread)).toBe(false); - }); - it("notices a changed wake countdown label", () => { const snoozedLayout = buildThreadListV2Items({ threads: [ @@ -1652,14 +1507,6 @@ describe("Working section beta", () => { expect(off.workingCount).toBe(0); }); - it("shows working rows as cards when expanded, or only the selected one when collapsed", () => { - expect(build({ workingShelfExpanded: true }).items.at(-1)).toMatchObject({ - thread: { id: "working" }, - variant: "card", - }); - expect(ids(build({ selectedThreadKey: `${environmentId}:working` })).at(-1)).toBe("working"); - }); - it("orders the inbox by the latest return this device observed", () => { const layout = build({ inboxReturnAt: (thread) => diff --git a/apps/mobile/src/features/threads/threadQueueControlPresentation.test.ts b/apps/mobile/src/features/threads/threadQueueControlPresentation.test.ts index 8808f1f6d55f6..14751cedd4025 100644 --- a/apps/mobile/src/features/threads/threadQueueControlPresentation.test.ts +++ b/apps/mobile/src/features/threads/threadQueueControlPresentation.test.ts @@ -33,32 +33,6 @@ describe("threadQueueControlPresentation", () => { ); }); - it("disables edge reorder controls and busy dismissal", () => { - const first = resolveThreadQueueRowControls({ - busy: false, - canPromoteToSteer: false, - canReorder: true, - index: 0, - queuedCount: 2, - text: "First", - }); - const busy = resolveThreadQueueRowControls({ - busy: true, - canPromoteToSteer: true, - canReorder: true, - index: 0, - queuedCount: 1, - text: "Queued message", - }); - - expect(first.canMoveUp).toBe(false); - expect(first.canMoveDown).toBe(true); - expect(first.canSteer).toBe(false); - expect(busy.canDismiss).toBe(false); - expect(busy.canMoveUp).toBe(false); - expect(busy.canSteer).toBe(false); - }); - it("keeps the row already open in the composer from being reopened or steered", () => { const editing = resolveThreadQueueRowControls({ busy: false, diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts index 810b6cb12f24a..f038fe8f9c25f 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.test.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.test.ts @@ -59,18 +59,6 @@ describe("mobile slash commands", () => { }, ); - it("does not offer a native command inside the message", () => { - expect( - buildComposerSlashCommandItems({ - query: "plan", - atMessageStart: false, - hasThread: false, - allowInteractionMode: true, - selectedProviderStatus: antigravity, - }), - ).toEqual([]); - }); - it("still applies the T3 plan command for supported providers", () => { const items = buildComposerSlashCommandItems({ query: "plan", diff --git a/apps/mobile/src/features/updates/app-updates.test.ts b/apps/mobile/src/features/updates/app-updates.test.ts index 4a9ae2736934c..73c95d01d8eb1 100644 --- a/apps/mobile/src/features/updates/app-updates.test.ts +++ b/apps/mobile/src/features/updates/app-updates.test.ts @@ -194,28 +194,6 @@ describe("runAppUpdateCheck", () => { expect(client.reloadAsync).toHaveBeenCalledOnce(); }); - it("reports manual failures without continuing the update", async () => { - const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); - const client = makeUpdateClient({ - checkForUpdateAsync: vi.fn(async () => { - throw new Error("offline"); - }), - }); - const failures: string[] = []; - const states: AppUpdateCheckState[] = []; - - await runAppUpdateCheck({ - client, - onFailure: (message) => failures.push(message), - onStateChange: (state) => states.push(state), - }); - - expect(client.fetchUpdateAsync).not.toHaveBeenCalled(); - expect(failures).toEqual(["offline"]); - expect(states).toEqual(["checking", "idle"]); - reportError.mockRestore(); - }); - it.each(["ERR_NOT_AVAILABLE_IN_DEV_CLIENT", "ERR_UPDATES_DISABLED"])( "treats Expo's %s failure as an unavailable update check", async (code) => { @@ -280,38 +258,6 @@ describe("runAppUpdateCheck", () => { expect(client.checkForUpdateAsync).toHaveBeenCalledTimes(2); }); - it("forwards failures to a manual check coalesced with the launch check", async () => { - const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); - let rejectCheck!: (error: Error) => void; - const checkResult = new Promise<{ - readonly isAvailable: boolean; - readonly isRollBackToEmbedded: boolean; - }>((_resolve, reject) => { - rejectCheck = reject; - }); - const client = makeUpdateClient({ - checkForUpdateAsync: vi.fn(() => checkResult), - }); - const checkOnLaunch = createAppUpdateLaunchCheck(client); - const failures: string[] = []; - const manualStates: AppUpdateCheckState[] = []; - - const launchCheck = checkOnLaunch(); - const manualCheck = runAppUpdateCheck({ - client, - onFailure: (message) => failures.push(message), - onStateChange: (state) => manualStates.push(state), - }); - - rejectCheck(new Error("offline")); - await Promise.all([launchCheck, manualCheck]); - - expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); - expect(failures).toEqual(["offline"]); - expect(manualStates).toEqual(["checking", "idle"]); - reportError.mockRestore(); - }); - it("publishes the in-flight check before a state callback can re-enter", async () => { let resolveCheck!: (result: { readonly isAvailable: boolean; diff --git a/apps/mobile/src/features/usage/usageEnvironmentSelection.test.ts b/apps/mobile/src/features/usage/usageEnvironmentSelection.test.ts index dcec7bf47b314..2492bbbed9384 100644 --- a/apps/mobile/src/features/usage/usageEnvironmentSelection.test.ts +++ b/apps/mobile/src/features/usage/usageEnvironmentSelection.test.ts @@ -16,10 +16,6 @@ describe("usage environment selection", () => { expect(toggleUsageEnvironment(selected, environments, b)).toBeNull(); }); - it("can deselect the last environment", () => { - expect(toggleUsageEnvironment(new Set([a]), environments, a)).toEqual(new Set()); - }); - it("does not count removed IDs toward selecting all current environments", () => { expect(toggleUsageEnvironment(new Set([a, removed]), environments, b)).toEqual(new Set([a, b])); }); diff --git a/apps/mobile/src/features/workspace/android-workspace-navigation-colors.test.ts b/apps/mobile/src/features/workspace/android-workspace-navigation-colors.test.ts index 961e7b0a629d4..e8f29a9cf8b67 100644 --- a/apps/mobile/src/features/workspace/android-workspace-navigation-colors.test.ts +++ b/apps/mobile/src/features/workspace/android-workspace-navigation-colors.test.ts @@ -48,7 +48,7 @@ describe("Android workspace navigation colors", () => { }, ); - it.each(["#6750a4", "#006a6a", "#9c4146"])( + it.each(["#6750a4", "#9c4146"])( "keeps Material You selection visible for system seed %s", (seed) => { for (const appearance of ["light", "dark"] as const) { diff --git a/apps/mobile/src/lib/android-workspace-navigation.test.ts b/apps/mobile/src/lib/android-workspace-navigation.test.ts index 8900668757411..ac163d7721490 100644 --- a/apps/mobile/src/lib/android-workspace-navigation.test.ts +++ b/apps/mobile/src/lib/android-workspace-navigation.test.ts @@ -110,18 +110,6 @@ describe("Android workspace navigation", () => { expect(names(apply(second, StackActions.pop()))).toEqual(["Home"]); }); - it("dismisses nested content and overlays when the sidebar selects a peer", () => { - let state = legacyHistory(); - state = apply( - state, - StackActions.push("PullRequestFile", { ...pullRequest.params, path: "src/main.ts" }), - ); - state = apply(state, StackActions.push("SettingsSheet")); - const selected = select(state, thread); - expect(names(selected)).toEqual(["Home", "Thread"]); - expect(selected.routes[1]?.params).toEqual(thread.params); - }); - it("creates Home beneath cold-linked details and gives direct list escape", () => { const initial = router.getInitialState(options); const cold = apply(initial, CommonActions.reset({ index: 0, routes: [pullRequest] })); @@ -149,21 +137,18 @@ describe("Android workspace navigation", () => { }, ); - it.each([ - { environmentId: "environment-b" }, - { projectId: "project-b" }, - { repository: "owner/other" }, - { host: "git.example.test" }, - { number: 43 }, - ])("does not reuse another PR's key when its scope differs: %o", (change) => { - const previous = select(router.getInitialState(options), pullRequest); - const state = select(previous, { - name: "PullRequest", - params: { ...pullRequest.params, ...change }, - }); - expect(state.routes[1]?.key).not.toBe(previous.routes[1]?.key); - expect(state.routes[1]?.params).toEqual({ ...pullRequest.params, ...change }); - }); + it.each([{ environmentId: "environment-b" }, { repository: "owner/other" }])( + "does not reuse another PR's key when its scope differs: %o", + (change) => { + const previous = select(router.getInitialState(options), pullRequest); + const state = select(previous, { + name: "PullRequest", + params: { ...pullRequest.params, ...change }, + }); + expect(state.routes[1]?.key).not.toBe(previous.routes[1]?.key); + expect(state.routes[1]?.params).toEqual({ ...pullRequest.params, ...change }); + }, + ); it("returns to a distinct previous thread, otherwise to the workspace list", () => { const parent = select(router.getInitialState(options), thread); @@ -238,14 +223,6 @@ describe("Android workspace navigation", () => { ).toBe(false); }); - it("drops stale detail history on a tab switch while retaining Home", () => { - const previous = legacyHistory(); - const state = select(previous); - expect(names(state)).toEqual(["Home"]); - expect(state.routes[0]?.key).toBe(previous.routes[0]?.key); - expect(state.index).toBe(0); - }); - it("resets the root stack when a nested Settings stack has focus", () => { const root = apply(router.getInitialState(options), StackActions.push("SettingsSheet")); const settings = router.getInitialState({ diff --git a/apps/mobile/src/lib/attachmentDownload.test.ts b/apps/mobile/src/lib/attachmentDownload.test.ts index b3afd6a9a7dfe..07a0a80a0c6bd 100644 --- a/apps/mobile/src/lib/attachmentDownload.test.ts +++ b/apps/mobile/src/lib/attachmentDownload.test.ts @@ -72,7 +72,6 @@ vi.mock("./uuid", () => ({ uuidv4: mocks.uuid })); vi.mock("./shareFileFromSource", () => ({ shareFileFromSource: mocks.shareFromSource })); import { - openAttachmentInViewer, downloadAndShareAttachment, downloadAttachmentForPreview, shareLocalAttachment, @@ -137,12 +136,8 @@ describe("downloadAndShareAttachment", () => { it.each([ ["../../résumé.pdf", "résumé.pdf"], ["C:\\folder\\clip.mp4", "clip.mp4"], - ["a?query#part%2F.txt", "a?query#part%2F.txt"], - ["Report #5 - 100%.pdf", "Report #5 - 100%.pdf"], ["..", "attachment"], - [" ", "attachment"], ["\ud800file\u0000.txt", "_file_.txt"], - [".env", ".env"], ])("uses a safe basename for %j", async (name, expected) => { await downloadAndShareAttachment({ ...input, @@ -192,23 +187,6 @@ describe("downloadAndShareAttachment", () => { expect(mocks.share).not.toHaveBeenCalled(); }); - it("does not open a late download after cancellation", async () => { - const started = Promise.withResolvers<{ uri: string }>(); - const download = Promise.withResolvers<{ uri: string }>(); - mocks.download.mockImplementation((_url: string, file: { uri: string }) => { - started.resolve(file); - return download.promise; - }); - const controller = new AbortController(); - const task = downloadAndShareAttachment({ ...input, signal: controller.signal }); - const file = await started.promise; - controller.abort(); - download.resolve(file); - await task; - expect(mocks.share).not.toHaveBeenCalled(); - expect(mocks.deleted).toHaveBeenCalledTimes(1); - }); - it("retains an export when its row unmounts during the native handoff", async () => { const opened = Promise.withResolvers(); const share = Promise.withResolvers(); @@ -366,19 +344,3 @@ describe("attachment preview files", () => { expect(mocks.deleted).toHaveBeenCalledTimes(1); }); }); - -describe("document viewer handoff", () => { - it("cleans up when no viewer handles the document", async () => { - mocks.open.mockRejectedValue(new Error("No viewer")); - await expect( - openAttachmentInViewer({ - uri: input.url, - attachment: input.attachment, - signal: new AbortController().signal, - }), - ).rejects.toThrow("No viewer"); - expect(mocks.download).toHaveBeenCalledTimes(1); - expect(mocks.deleted).toHaveBeenCalledTimes(1); - expect(isForegroundHandoffActive()).toBe(false); - }); -}); diff --git a/apps/mobile/src/lib/attachmentUpload.test.ts b/apps/mobile/src/lib/attachmentUpload.test.ts index 9d46e1f55f9bf..70d933805d9cb 100644 --- a/apps/mobile/src/lib/attachmentUpload.test.ts +++ b/apps/mobile/src/lib/attachmentUpload.test.ts @@ -139,10 +139,6 @@ const documentPickedImage = { } as const satisfies DraftComposerAttachment; describe("validateDraftFileAttachments", () => { - it("allows legacy image-only sends without server config", () => { - expect(validateDraftFileAttachments({ attachments: [image], serverConfig: null })).toBeNull(); - }); - it("blocks files while config is unknown or file uploads are unsupported", () => { expect(validateDraftFileAttachments({ attachments: [file], serverConfig: null })).toBe( "Server attachment support is still loading.", @@ -294,24 +290,6 @@ describe("prepareTurnAttachments", () => { expect(mocks.runAtomCommand).toHaveBeenCalledTimes(1); }); - it("keeps existing image attachments on the legacy wire path", async () => { - const prepared = await prepareTurnAttachments({ environmentId, attachments: [image] }); - - expect(prepared.status).toBe("ready"); - if (prepared.status !== "ready") return; - expect(prepared.attachments).toEqual([ - { - type: "image", - name: "screenshot.png", - mimeType: "image/png", - sizeBytes: 3, - dataUrl: "data:image/png;base64,YWJj", - }, - ]); - expect(prepared.pendingAttachmentIds).toEqual([]); - expect(mocks.upload).not.toHaveBeenCalled(); - }); - it("inlines a file-backed image lazily when the server lacks image uploads", async () => { const prepared = await prepareTurnAttachments({ environmentId, @@ -333,6 +311,7 @@ describe("prepareTurnAttachments", () => { expect(mocks.upload).not.toHaveBeenCalled(); }); + // regression: #9713 read file-backed image drafts before enabling them it("reads restored images from the current iOS document container for legacy sends", async () => { const fileName = "33333333-3333-4333-8333-333333333333-photo.png"; mocks.documentUri = @@ -348,6 +327,7 @@ describe("prepareTurnAttachments", () => { ); }); + // regression: #9713 read file-backed image drafts before enabling them it("rejects a legacy image without inline bytes or a file", async () => { const { fileUri: _, ...missingImage } = fileBackedImage; await expect( @@ -379,6 +359,7 @@ describe("prepareTurnAttachments", () => { }, ); + // regression: #9713 read file-backed image drafts before enabling them it("uploads a file-backed image from its owned copy without staging base64", async () => { const prepared = await prepareTurnAttachments({ environmentId, @@ -542,30 +523,6 @@ describe("prepareTurnAttachments", () => { ]); }); - it("keeps the uploaded id when a document-picked picture uploads as an image", () => { - expect( - withUploadedMobileAttachmentReferences({ - environmentId, - attachments: [documentPickedImage], - uploadedAttachments: [ - { - type: "image", - id: "pending-promoted-png", - name: documentPickedImage.name, - mimeType: "image/png", - sizeBytes: documentPickedImage.sizeBytes, - }, - ], - }), - ).toEqual([ - { - ...documentPickedImage, - uploadedAttachmentId: "pending-promoted-png", - uploadEnvironmentId: environmentId, - }, - ]); - }); - it("reuses a pending file upload from a previous outbox attempt", async () => { const previouslyUploaded = { ...file, @@ -775,20 +732,6 @@ describe("prepareTurnAttachments", () => { expect(result.status).toBe("abandoned"); expect(removeCallsFor(MINTED_ID)).toBe(1); }); - - it("deletes the minted uploads when persisting the references throws", async () => { - await expect( - prepareTurnAttachments({ - environmentId, - attachments: [file], - persistUploadedReferences: async () => { - throw new Error("draft write failed"); - }, - }), - ).rejects.toThrow("draft write failed"); - - expect(removeCallsFor(MINTED_ID)).toBe(1); - }); }); describe("releasePendingAttachmentUploads", () => { diff --git a/apps/mobile/src/lib/composerContextClipboard.test.ts b/apps/mobile/src/lib/composerContextClipboard.test.ts index 6367df41c7c83..dffc8b00a5d74 100644 --- a/apps/mobile/src/lib/composerContextClipboard.test.ts +++ b/apps/mobile/src/lib/composerContextClipboard.test.ts @@ -244,6 +244,7 @@ describe("mobile context clipboard imports", () => { expect(mocks.download).not.toHaveBeenCalled(); }); + // regression: #18322 don't send or paste attached threads a machine can't read it("reports foreign threads and failed files together while keeping the rest of a paste", async () => { mocks.download.mockRejectedValueOnce(new Error("source disconnected")); const result = await importComposerContextClipboard( diff --git a/apps/mobile/src/lib/composerFiles.test.ts b/apps/mobile/src/lib/composerFiles.test.ts index c562fda9c1acf..14d1d630439b5 100644 --- a/apps/mobile/src/lib/composerFiles.test.ts +++ b/apps/mobile/src/lib/composerFiles.test.ts @@ -222,6 +222,7 @@ describe("composer file attachments", () => { expect(result.images).toEqual([expect.objectContaining({ mimeType: "image/jpeg" })]); }); + // regression: #11440 render photo library picks to a bounded JPEG off the JS thread it("renders a supported original whose size cannot be measured", async () => { mocks.pickMedia.mockResolvedValue({ canceled: false, @@ -265,6 +266,7 @@ describe("composer file attachments", () => { expect(result.error).toBe("Failed to read 'missing.gif'."); }); + // regression: #11440 render photo library picks to a bounded JPEG off the JS thread it("reports a photo the native renderer cannot decode", async () => { mocks.manipulate.mockImplementation(() => ({ resize: () => { @@ -371,13 +373,6 @@ describe("composer file attachments", () => { }); it.each([ - { - reason: "picker size exceeds the server limit", - reported: 2 * 1024 * 1024, - stored: 42, - limit: 1024 * 1024, - error: "'clip.mov' exceeds the 1 MB attachment limit.", - }, { reason: "actual size exceeds the server limit", reported: 42, @@ -563,18 +558,6 @@ describe("composer file attachments", () => { expect(mocks.open).not.toHaveBeenCalled(); }); - it("reports picker failures and releases the foreground handoff", async () => { - mocks.pickFile.mockRejectedValue(new Error("The document provider is unavailable.")); - - await expect(pickComposerFiles({ existingCount: 0 })).resolves.toEqual({ - files: [], - error: "The document provider is unavailable.", - }); - - expect(isForegroundHandoffActive()).toBe(false); - expect(mocks.copy).not.toHaveBeenCalled(); - }); - it("does not open the picker when the draft has no remaining attachment slots", async () => { await expect(pickComposerFiles({ existingCount: 100 })).resolves.toEqual({ files: [], @@ -605,27 +588,6 @@ describe("composer file attachments", () => { expect(mocks.copy).not.toHaveBeenCalled(); }); - it("never accepts files above the 50 MB contract limit", async () => { - mocks.pickFile.mockResolvedValue({ - canceled: false, - assets: [ - { - uri: "file:///downloads/archive.zip", - name: "archive.zip", - mimeType: "application/zip", - size: 51 * 1024 * 1024, - }, - ], - }); - - await expect( - pickComposerFiles({ existingCount: 0, maxBytes: 80 * 1024 * 1024 }), - ).resolves.toEqual({ - files: [], - error: "'archive.zip' exceeds the 50 MB attachment limit.", - }); - }); - it("rejects a file that grew after the picker reported its size", async () => { mocks.pickFile.mockResolvedValue({ canceled: false, @@ -691,26 +653,6 @@ describe("composer file attachments", () => { ); }); - it("reports an empty file without calling it oversized", async () => { - mocks.size.mockReturnValue(0); - mocks.pickFile.mockResolvedValue({ - canceled: false, - assets: [ - { - uri: "file:///downloads/empty.txt", - name: "empty.txt", - mimeType: "text/plain", - size: 0, - }, - ], - }); - - await expect(pickComposerFiles({ existingCount: 0 })).resolves.toEqual({ - files: [], - error: "'empty.txt' is empty or could not be read.", - }); - }); - it.each([0, undefined])("copies an Android SAF file when the picker size is %s", async (size) => { const reader = { readBytes: vi diff --git a/apps/mobile/src/lib/composerImages.test.ts b/apps/mobile/src/lib/composerImages.test.ts index ab3653f5264ba..9804a6d0e5851 100644 --- a/apps/mobile/src/lib/composerImages.test.ts +++ b/apps/mobile/src/lib/composerImages.test.ts @@ -103,12 +103,7 @@ vi.mock("./uuid", () => ({ uuidv4: () => "attachment-id", })); -import { - convertPastedImagesToAttachments, - createPastedTextComposerAttachment, - isOwnedPastedImageUri, - pasteComposerClipboard, -} from "./composerImages"; +import { convertPastedImagesToAttachments, pasteComposerClipboard } from "./composerImages"; describe("composer clipboard paste", () => { beforeEach(() => { @@ -195,16 +190,6 @@ describe("native pasted image cleanup", () => { files.clear(); }); - it("recognizes only files created in the native composer paste directory", () => { - expect( - isOwnedPastedImageUri( - "file:///private/var/mobile/Containers/Data/Application/app/tmp/t3-composer-paste/id.png", - ), - ).toBe(true); - expect(isOwnedPastedImageUri("file:///private/var/mobile/photos/id.png")).toBe(false); - expect(isOwnedPastedImageUri("https://example.com/t3-composer-paste/id.png")).toBe(false); - }); - it("converts owned files to data-backed previews and deletes the source", async () => { const uri = "file:///private/var/mobile/Containers/Data/Application/app/tmp/t3-composer-paste/id.png"; @@ -243,26 +228,6 @@ describe("native pasted image cleanup", () => { expect(files.get(overflow)?.deleted).toBe(true); expect(files.get(userOwned)?.deleted).toBe(false); }); - - it("persists folded text unchanged in the app-owned attachment directory", async () => { - const text = "first line\nUnicode: 🙂\n"; - const attachment = await createPastedTextComposerAttachment({ - text, - name: "pasted-text.txt", - maxBytes: 1024, - }); - - expect(attachment).toEqual({ - id: "attachment-id", - type: "file", - name: "pasted-text.txt", - mimeType: "text/plain;charset=utf-8", - sizeBytes: new TextEncoder().encode(text).byteLength, - fileUri: "file:///documents/t3-composer-attachments/attachment-id-pasted-text.txt", - source: { _tag: "pasted-text" }, - }); - expect(files.get(attachment.fileUri)?.text).toBe(text); - }); }); describe("composerStripAttachments", () => { @@ -298,34 +263,4 @@ describe("composerStripAttachments", () => { "vid-1", ]); }); - - it("treats a picture picked through the document picker as media", async () => { - const { composerStripAttachments } = await import("./composerImages"); - const pickedImage = { - id: "pick-1", - type: "file" as const, - name: "photo.png", - mimeType: "image/png", - sizeBytes: 30, - fileUri: "file:///photo.png", - }; - expect(composerStripAttachments([pickedImage] as never).map((a) => a.id)).toEqual(["pick-1"]); - }); -}); - -describe("composerAttachmentInlineUri", () => { - it("offers the inline bytes of a picture that owns no file", async () => { - const { composerAttachmentInlineUri } = await import("./composerImages"); - expect( - composerAttachmentInlineUri({ - id: "img-1", - type: "image", - name: "IMG_0111.jpg", - mimeType: "image/jpeg", - sizeBytes: 9_100_000, - dataUrl: "data:image/jpeg;base64,AAAA", - previewUri: "ph://asset", - } as never), - ).toBe("data:image/jpeg;base64,AAAA"); - }); }); diff --git a/apps/mobile/src/lib/connection.test.ts b/apps/mobile/src/lib/connection.test.ts index d40cfd6f939d7..bb0a9c94ca91e 100644 --- a/apps/mobile/src/lib/connection.test.ts +++ b/apps/mobile/src/lib/connection.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { EnvironmentId } from "@t3tools/contracts"; -import { isRelayManagedConnection, toStableSavedRemoteConnection } from "./connection"; +import { toStableSavedRemoteConnection } from "./connection"; const mobilePlatform = vi.hoisted(() => ({ OS: "ios" as "ios" | "android" })); const mobileDevice = vi.hoisted(() => ({ @@ -37,11 +37,6 @@ describe("mobile remote connection records", () => { mobileDevice.modelName = "iPhone 15 Pro"; }); - it("keeps existing DPoP tunnel records read-only after upgrading", () => { - expect(isRelayManagedConnection({ authenticationMethod: "dpop" })).toBe(true); - expect(isRelayManagedConnection({ authenticationMethod: "bearer" })).toBe(false); - }); - it("drops short-lived managed environment credentials from stable records", () => { const connection = { environmentId: EnvironmentId.make("environment-1"), diff --git a/apps/mobile/src/lib/copyTextWithHaptic.test.ts b/apps/mobile/src/lib/copyTextWithHaptic.test.ts deleted file mode 100644 index 89d43f18894e3..0000000000000 --- a/apps/mobile/src/lib/copyTextWithHaptic.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; - -const mocks = vi.hoisted(() => ({ - impactAsync: vi.fn(), - selectionAsync: vi.fn(), - setStringAsync: vi.fn(), -})); - -vi.mock("expo-clipboard", () => ({ - setStringAsync: mocks.setStringAsync, -})); - -vi.mock("expo-haptics", () => ({ - ImpactFeedbackStyle: { - Light: "light", - }, - impactAsync: mocks.impactAsync, - selectionAsync: mocks.selectionAsync, -})); - -import { tryCopyTextWithHaptic } from "./copyTextWithHaptic"; - -describe("copyTextWithHaptic", () => { - beforeEach(() => { - vi.clearAllMocks(); - mocks.setStringAsync.mockReturnValue(new Promise(() => undefined)); - mocks.impactAsync.mockResolvedValue(undefined); - mocks.selectionAsync.mockResolvedValue(undefined); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("reports structured failures without including clipboard contents", async () => { - const content = "https://accounts.google.com/auth?state=private-state&code=private-code"; - const clipboardCause = new Error(`Cannot copy ${content}`); - const hapticCause = new Error(`Native failure for ${content}`); - const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); - mocks.setStringAsync.mockRejectedValueOnce(clipboardCause); - mocks.impactAsync.mockRejectedValueOnce(hapticCause); - - await tryCopyTextWithHaptic(content, { target: "provider-sign-in-link" }); - - expect(consoleError).toHaveBeenCalledWith( - "Failed to copy provider-sign-in-link to the clipboard.", - expect.objectContaining({ - _tag: "CopyTextClipboardWriteError", - target: "provider-sign-in-link", - }), - ); - expect(consoleError).toHaveBeenCalledWith( - "Failed to trigger light-impact haptic feedback after copying provider-sign-in-link.", - expect.objectContaining({ _tag: "CopyTextHapticFeedbackError", feedback: "light-impact" }), - ); - const diagnostics = JSON.stringify(consoleError.mock.calls); - expect(diagnostics).not.toContain("private-state"); - expect(diagnostics).not.toContain("private-code"); - expect(diagnostics).not.toContain("cause"); - }); -}); diff --git a/apps/mobile/src/lib/enrichedLinkPresentation.test.ts b/apps/mobile/src/lib/enrichedLinkPresentation.test.ts index 81c750db8aeec..3ceb536926dfb 100644 --- a/apps/mobile/src/lib/enrichedLinkPresentation.test.ts +++ b/apps/mobile/src/lib/enrichedLinkPresentation.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import type { ComposerContextId, ReviewCommentContextRecord } from "@t3tools/contracts"; import { - enrichedContextLinkPresentation, enrichedLinkVariantPattern, enrichedSkillLinkRegex, ENRICHED_INLINE_FILE_LINK_REGEX, @@ -48,33 +46,4 @@ describe("Enriched link presentation", () => { expect(pattern.test(`${url}/child`)).toBe(false); expect(pattern.test("file:///workspace/app1Xtsx?line=34")).toBe(false); }); - - it("uses context identity for PR status even when labels are identical", () => { - const record: ReviewCommentContextRecord = { - version: 1, - contextId: "review_1" as ComposerContextId, - kind: "review-comment", - label: "Review", - sectionId: "pull-request:7", - sectionTitle: "Review", - filePath: "app.tsx", - startIndex: 0, - endIndex: 1, - rangeLabel: "line 1", - text: "Comment", - diff: "", - pullRequest: { - number: 7, - title: "Review", - url: "https://github.com/example/repo/pull/7", - headBranch: "feature", - baseBranch: "main", - state: "merged", - isDraft: false, - }, - }; - expect( - enrichedContextLinkPresentation("t3-context://v1/review-comment/review_1", [record]), - ).toEqual({ accent: "#8a70dd", icon: { kind: "chip", name: "git-merge" } }); - }); }); diff --git a/apps/mobile/src/lib/filePreview.test.ts b/apps/mobile/src/lib/filePreview.test.ts index 50be5369c7d3f..a48a4bd11462a 100644 --- a/apps/mobile/src/lib/filePreview.test.ts +++ b/apps/mobile/src/lib/filePreview.test.ts @@ -4,12 +4,9 @@ import { isPdfFile } from "./filePreview"; describe("PDF preview detection", () => { it.each([ - [{ name: "download", mimeType: "application/pdf" }, true], [{ name: "download", mimeType: "APPLICATION/PDF; charset=binary" }, true], - [{ name: "Report.PDF", mimeType: "application/octet-stream" }, true], [{ name: "https://example.com/report.pdf?signature=abc#page=2" }, true], [{ name: "report.pdf", mimeType: "text/plain" }, false], - [{ name: "report.pdf.exe" }, false], [{ name: "https://example.com/page?download=report.pdf" }, false], ])("classifies %j as %s", (file, expected) => { expect(isPdfFile(file)).toBe(expected); diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts index 0f4b5a9652100..0552d550391c5 100644 --- a/apps/mobile/src/lib/layout.test.ts +++ b/apps/mobile/src/lib/layout.test.ts @@ -12,7 +12,6 @@ import { describe("thread work-log text sizing", () => { it.each([ { baseFontSize: 16, fontScale: 1.25, estimatedRowHeight: 28 }, - { baseFontSize: 16, fontScale: 2, estimatedRowHeight: 38 }, { baseFontSize: 22, fontScale: 2, estimatedRowHeight: 52 }, ])( "measures accessibility text instead of locking it to the estimate: %j", @@ -38,6 +37,7 @@ describe("thread work-log text sizing", () => { }); describe("deriveThreadFeedInitialContentInset", () => { + // regression: #5585 land the first thread open above the composer on Android it("does not double native iOS insets", () => { expect( deriveThreadFeedInitialContentInset({ @@ -50,6 +50,7 @@ describe("deriveThreadFeedInitialContentInset", () => { }); describe("isPrimarySidebarPinned", () => { + // regression: #20406 sidebar toggle works on iPad and iPhone Duo it("keeps compact native roots pinned so search cannot hide the sidebar for a later split", () => { expect( isPrimarySidebarPinned({ pathname: "/", nativeWorkspace: true, usesSplitView: false }), @@ -75,24 +76,6 @@ describe("deriveWorkspacePaneLayout", () => { auxiliaryPaneVisible: true, }); }); - - it("restores the thread sidebar when the file inspector is hidden", () => { - const layout = deriveLayout({ width: 1_024, height: 1_366 }); - - expect( - deriveWorkspacePaneLayout({ - layout, - viewportWidth: 1_024, - primarySidebarPreferredVisible: true, - auxiliaryPanePreferredVisible: false, - auxiliaryPaneRole: "inspector", - }), - ).toMatchObject({ - primarySidebarVisible: true, - primarySidebarSuppressedByAuxiliary: false, - auxiliaryPaneVisible: false, - }); - }); }); describe("alignListPaneToHinge", () => { diff --git a/apps/mobile/src/lib/markdownHtml.test.ts b/apps/mobile/src/lib/markdownHtml.test.ts index a77a77e9a9cde..e9e1fc219696b 100644 --- a/apps/mobile/src/lib/markdownHtml.test.ts +++ b/apps/mobile/src/lib/markdownHtml.test.ts @@ -3,14 +3,6 @@ import { markdownHtmlContent, markdownWithHtml, parseMarkdownDocument } from "./ import { markdownImageSources } from "./markdownImageSources"; describe("HTML in native Markdown", () => { - it("leaves code inside an HTML details block literal", () => { - const content = - '
\nExample\n\n```html\n\n```\n\nUse `` here.\n
'; - const result = markdownHtmlContent(content); - expect(result).toContain('```html\n\n```'); - expect(result).toContain("Use `` here."); - expect(result).not.toContain("
"); - }); it("preserves screenshot URLs and captions from GitHub HTML image embeds", () => { expect( markdownHtmlContent( diff --git a/apps/mobile/src/lib/markdownLinks.test.ts b/apps/mobile/src/lib/markdownLinks.test.ts index f3de6bdeeb182..101a064b8e61f 100644 --- a/apps/mobile/src/lib/markdownLinks.test.ts +++ b/apps/mobile/src/lib/markdownLinks.test.ts @@ -1,17 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { resolveMarkdownLinkIcon, resolveMarkdownLinkPresentation } from "./markdownLinks"; - -describe("resolveMarkdownLinkIcon", () => { - it("gives GitHub hosts the brand mark and everything else the generic glyph", () => { - expect(resolveMarkdownLinkIcon("github.com")).toBe("github"); - expect(resolveMarkdownLinkIcon("GitHub.com")).toBe("github"); - expect(resolveMarkdownLinkIcon("gist.github.com")).toBe("github"); - expect(resolveMarkdownLinkIcon("github.community")).toBeNull(); - expect(resolveMarkdownLinkIcon("notgithub.com")).toBeNull(); - expect(resolveMarkdownLinkIcon("example.com")).toBeNull(); - }); -}); +import { resolveMarkdownLinkPresentation } from "./markdownLinks"; describe("resolveMarkdownLinkPresentation", () => { it("opens relative directory links as workspace paths", () => { @@ -81,6 +70,7 @@ describe("resolveMarkdownLinkPresentation", () => { }); }); + // regression: #8098 open agent file links in the file viewer it("recognizes spaced relative paths", () => { expect(resolveMarkdownLinkPresentation("docs/My%20Folder/checklist.xml")).toMatchObject({ kind: "file", @@ -89,16 +79,6 @@ describe("resolveMarkdownLinkPresentation", () => { }); }); - it("extracts line fragments from relative file links", () => { - expect(resolveMarkdownLinkPresentation("src/main.ts#L18C2")).toMatchObject({ - kind: "file", - path: "src/main.ts", - line: 18, - column: 2, - label: "main.ts · L18:C2", - }); - }); - it("does not style app routes as file links", () => { expect(resolveMarkdownLinkPresentation("/chat/settings")).toEqual({ kind: "link", diff --git a/apps/mobile/src/lib/markdownMedia.test.ts b/apps/mobile/src/lib/markdownMedia.test.ts index d1773336f3006..33fec670e41ea 100644 --- a/apps/mobile/src/lib/markdownMedia.test.ts +++ b/apps/mobile/src/lib/markdownMedia.test.ts @@ -69,7 +69,7 @@ describe("resolveMarkdownMediaPreview", () => { }); }); - it.each(["./image.png", "./clip.mp4", "./report.pdf"])( + it.each(["./image.png", "./report.pdf"])( "uses the draft workspace when no thread exists: %s", (href) => { expect( diff --git a/apps/mobile/src/lib/materialYouTheme.test.ts b/apps/mobile/src/lib/materialYouTheme.test.ts deleted file mode 100644 index 14c661501c4bf..0000000000000 --- a/apps/mobile/src/lib/materialYouTheme.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; - -import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; -import type { MaterialYouPalette } from "./materialYouPalette"; -import { materialYouPaletteToMobileThemeVariables } from "./materialYouTheme.android"; - -const palette: MaterialYouPalette = { - primary: "#6750A4FF", - onPrimary: "#FFFFFFFF", - primaryContainer: "#EADDFFFF", - onPrimaryContainer: "#21005DFF", - inversePrimary: "#D0BCFFFF", - secondaryContainer: "#E8DEF8FF", - onSecondaryContainer: "#1D192BFF", - tertiary: "#7D5260FF", - tertiaryContainer: "#FFD8E4FF", - onTertiaryContainer: "#31111DFF", - surface: "#FFFBFEFF", - onSurface: "#1C1B1FFF", - onSurfaceVariant: "#49454FFF", - surfaceContainer: "#F3EDF7FF", - surfaceContainerHigh: "#ECE6F0FF", - surfaceContainerHighest: "#E6E0E9FF", - surfaceContainerLow: "#F7F2FAFF", - surfaceContainerLowest: "#FFFFFFFF", - errorContainer: "#F9DEDCFF", - onErrorContainer: "#410E0BFF", - outline: "#79747EFF", - outlineVariant: "#CAC4D0FF", - scrim: "#000000FF", -}; - -describe("Material You system colors", () => { - it("overrides the selected theme without mutating its base variables", () => { - const base = getMobileThemeRuntimeVariables("t3-code", "dark", "android"); - const snapshot = { ...base }; - - const variables = materialYouPaletteToMobileThemeVariables(palette, "dark", base); - - expect(base).toEqual(snapshot); - expect(variables["--color-screen"]).toBe(palette.surface); - expect(variables["--color-thread-canvas"]).toBe(palette.surfaceContainerLow); - expect(variables["--color-thread-selected"]).toBe(palette.surfaceContainer); - expect(variables["--color-header"]).toBe(palette.surfaceContainerHigh); - expect(variables["--color-grouped-card"]).toBe(palette.surfaceContainer); - expect(variables["--color-primary"]).toBe(palette.primary); - expect(variables["--color-placeholder"]).toBe("#1C1B1F9E"); - }); -}); diff --git a/apps/mobile/src/lib/mediaActions.test.ts b/apps/mobile/src/lib/mediaActions.test.ts index 74c07bc725aec..c7747cf2f2e88 100644 --- a/apps/mobile/src/lib/mediaActions.test.ts +++ b/apps/mobile/src/lib/mediaActions.test.ts @@ -178,21 +178,3 @@ it.each(["https://cdn.test/image.png", "file:///device/image.png"])( expect(uri.startsWith("file:") ? state.shareLocal : state.download).toHaveBeenCalledOnce(); }, ); - -it("shares device draft attachments without a host grant", async () => { - await useMediaActions({ - attachment: { - id: "draft", - type: "file", - fileUri: "file:///device/image.png", - name: "image.png", - mimeType: "image/png", - sizeBytes: 1, - }, - name: "image.png", - mimeType: "image/png", - }).share(); - - expect(state.refresh).not.toHaveBeenCalled(); - expect(state.shareDraft).toHaveBeenCalledOnce(); -}); diff --git a/apps/mobile/src/lib/menu-action-colors.test.ts b/apps/mobile/src/lib/menu-action-colors.test.ts deleted file mode 100644 index 6f7743c8a647e..0000000000000 --- a/apps/mobile/src/lib/menu-action-colors.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { MenuAction } from "@react-native-menu/menu"; -import { describe, expect, it } from "vite-plus/test"; - -import { withMenuActionIconColors } from "./menu-action-colors"; - -describe("withMenuActionIconColors", () => { - it.each(["#111111", "#eeeeee"])( - "gives icons a visible color at every menu depth for the %s theme", - (icon) => { - const actions: MenuAction[] = [ - { id: "photos", title: "Photos", image: "photo" }, - { - title: "Thread", - subactions: [ - { - title: "Pinned thread", - image: "pin", - subactions: [{ title: "Move up", image: "arrow.up" }], - }, - ], - }, - ]; - - const result = withMenuActionIconColors(actions, { icon, destructiveIcon: "#ff0000" }); - - expect(result[0]?.imageColor).toBe(icon); - expect(result[1]).not.toHaveProperty("imageColor"); - expect(result[1]?.subactions?.[0]?.imageColor).toBe(icon); - expect(result[1]?.subactions?.[0]?.subactions?.[0]?.imageColor).toBe(icon); - expect(actions[0]).not.toHaveProperty("imageColor"); - expect(actions[1]?.subactions?.[0]).not.toHaveProperty("imageColor"); - }, - ); -}); diff --git a/apps/mobile/src/lib/mobileTheme.test.ts b/apps/mobile/src/lib/mobileTheme.test.ts index fac8b782ae8c5..31fc6f83f11c0 100644 --- a/apps/mobile/src/lib/mobileTheme.test.ts +++ b/apps/mobile/src/lib/mobileTheme.test.ts @@ -20,8 +20,6 @@ import { DEFAULT_MOBILE_THEME_ID, flattenThemeColor, getMobileThemeVariables, - normalizeMobileThemeId, - normalizeMobileThemeMode, resolveMobileThemeIds, themeColorWithAlpha, themeColorToNativeColor, @@ -204,13 +202,6 @@ describe("mobile themes", () => { } }); - it("normalizes persisted theme preferences", () => { - expect(normalizeMobileThemeId("ocean")).toBe("ocean"); - expect(normalizeMobileThemeId("missing-theme")).toBe(DEFAULT_MOBILE_THEME_ID); - expect(normalizeMobileThemeMode("dark")).toBe("dark"); - expect(normalizeMobileThemeMode("sepia")).toBe("system"); - }); - it("migrates one theme choice to both appearances and preserves independent choices", () => { expect(resolveMobileThemeIds({ themeId: "grove" })).toEqual({ light: "grove", diff --git a/apps/mobile/src/lib/mobileThemeRuntime.test.ts b/apps/mobile/src/lib/mobileThemeRuntime.test.ts index ec810c105176a..b65a5e9391f6c 100644 --- a/apps/mobile/src/lib/mobileThemeRuntime.test.ts +++ b/apps/mobile/src/lib/mobileThemeRuntime.test.ts @@ -20,8 +20,4 @@ describe("mobileThemeRuntime", () => { expect(operations).toEqual([]); }); - - it("does no native work when persistence echoes an already-applied state", () => { - expect(createMobileThemeRuntimeOperations(initialState, initialState)).toEqual([]); - }); }); diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index 8edb1534238f5..11b9b0603be84 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -400,9 +400,7 @@ describe("mobile model options", () => { it.each([ ["disabled", { enabled: false }], - ["uninstalled", { installed: false }], ["signed out", { auth: { status: "unauthenticated" } }], - ["unavailable", { availability: "unavailable" }], ] as const)("keeps a %s provider's selection and known model details", (_state, update) => { const unavailableConfig = { ...config, @@ -624,20 +622,6 @@ describe("mobile model options", () => { expect(buildModelOptions(configWith(settings), null, undefined, projectId)).toEqual([]); }); - it("shows an instance the project enables even though the machine disables it", () => { - const settings: ServerSettings = { - ...DEFAULT_SERVER_SETTINGS, - providerInstances: { - [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: false }, - }, - projectSettingsOverrides: { - [projectId]: { providerInstances: { [instanceId]: { enabled: true } } }, - }, - }; - - expect(buildModelOptions(configWith(settings), null, undefined, projectId)).toHaveLength(1); - }); - it("hides a machine-disabled instance for a project with no override, even though another project's keep-alive left it running", () => { const otherProjectId = ProjectId.make("project-b"); const settings: ServerSettings = { @@ -806,6 +790,7 @@ describe("synchronized mobile model visibility", () => { ).toEqual(["second", "first", "custom"]); }); + // regression: #16816 sync model favorites and visibility across clients it("keeps custom models visible and restores built-in models when the server clears hidden models", () => { expect( buildModelOptions(config, { instanceId: provider, model: "custom" }, provider, null).map( diff --git a/apps/mobile/src/lib/openExternalUrl.test.ts b/apps/mobile/src/lib/openExternalUrl.test.ts index df54ed8e421ec..1dd1ff7db002b 100644 --- a/apps/mobile/src/lib/openExternalUrl.test.ts +++ b/apps/mobile/src/lib/openExternalUrl.test.ts @@ -18,36 +18,6 @@ afterEach(() => { }); describe("tryOpenExternalUrl", () => { - it("logs stable URL context without exposing the opening failure", async () => { - const cause = new Error("browser-unavailable-secret-sentinel"); - openURL.mockRejectedValue(cause); - const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); - - await expect( - tryOpenExternalUrl("https://github.com/pingdotgg/t3code/pull/1?token=secret", "pull-request"), - ).resolves.toBe(false); - - expect(consoleError).toHaveBeenCalledTimes(1); - const [message, attributes] = consoleError.mock.calls[0] ?? []; - expect(message).toBe("Failed to open pull-request URL with the https scheme."); - expect(attributes).toEqual( - expect.objectContaining({ - _tag: "ExternalUrlOpenError", - target: "pull-request", - scheme: "https", - host: "github.com", - stack: expect.stringContaining("ExternalUrlOpenError"), - }), - ); - expect(attributes).not.toHaveProperty("url"); - expect(attributes).not.toHaveProperty("cause"); - const diagnosticText = [message, ...Object.values(attributes as Record)] - .map(String) - .join("\n"); - expect(diagnosticText).not.toContain("token=secret"); - expect(diagnosticText).not.toContain("browser-unavailable-secret-sentinel"); - }); - it("keeps provider sign-in URLs unchanged and out of failure logs", async () => { const url = "https://accounts.google.com/o/oauth2/v2/auth?state=private-state&code_challenge=private-challenge&redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2F"; diff --git a/apps/mobile/src/lib/protectedData.test.ts b/apps/mobile/src/lib/protectedData.test.ts index 9690caf11f2f1..d8a7fd7f025ea 100644 --- a/apps/mobile/src/lib/protectedData.test.ts +++ b/apps/mobile/src/lib/protectedData.test.ts @@ -80,16 +80,7 @@ afterEach(async () => { describe("protected storage startup", () => { it.each([ { launch: "background", state: "background", checkRejects: false }, - { launch: "inactive", state: "inactive", checkRejects: false }, - { launch: "unknown", state: "unknown", checkRejects: false }, - { launch: "unset", state: null, checkRejects: false }, { launch: "background with a failed native check", state: "background", checkRejects: true }, - { - launch: "background without a native check", - state: "background", - nativeCheckSupported: false, - }, - { launch: "unknown without a native check", state: "unknown", nativeCheckSupported: false }, { launch: "unset without a native check", state: null, nativeCheckSupported: false }, ])( "keeps a locked $launch launch usable after activation without rebuilding its runtime", diff --git a/apps/mobile/src/lib/storage.test.ts b/apps/mobile/src/lib/storage.test.ts index fe972670e57b3..e5dc872abebff 100644 --- a/apps/mobile/src/lib/storage.test.ts +++ b/apps/mobile/src/lib/storage.test.ts @@ -138,14 +138,6 @@ describe("mobile connection storage", () => { await expect(loadPreferences()).resolves.toEqual({ baseFontSize: 17 }); }); - it("persists Material You independently for each appearance", async () => { - const themes = { lightThemeId: "material-you", darkThemeId: "ocean" } as const; - await savePreferencesPatch(themes); - await expect(loadPreferences()).resolves.toEqual(themes); - await savePreferencesPatch({ lightThemeId: "t3-chat" }); - await expect(loadPreferences()).resolves.toEqual({ ...themes, lightThemeId: "t3-chat" }); - }); - it("falls back to secure storage when SQLite cannot save preferences", async () => { mocks.setDatabaseFailures(true, true); await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 }); @@ -187,21 +179,6 @@ describe("mobile connection storage", () => { expect(mocks.getStoredValue("t3code.preferences.fallback")).toBeNull(); }); - it("ignores an invalid fallback even when it has a newer timestamp", async () => { - mocks.setPreferencesJson(JSON.stringify({ baseFontSize: 21 }), 30); - await mocks.setItemAsync( - "t3code.preferences.fallback", - JSON.stringify({ payload: "{", updatedAt: 40 }), - ); - const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); - - await expect(loadPreferences()).resolves.toEqual({ baseFontSize: 21 }); - expect(JSON.parse(mocks.getPreferencesJson() ?? "")).toEqual({ baseFontSize: 21 }); - expect(mocks.getStoredValue("t3code.preferences.fallback")).toBeNull(); - - warn.mockRestore(); - }); - it("keeps SQLite authoritative when stale legacy preferences remain", async () => { mocks.setPreferencesJson(JSON.stringify({ baseFontSize: 21 }), 30); await mocks.setItemAsync("t3code.preferences", JSON.stringify({ baseFontSize: 19 })); diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index 98afa13f06708..92570bc7c1019 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -463,6 +463,7 @@ describe("buildThreadFeed", () => { ]); }); + // regression: #21393 show plans and fold settled progress notes it("folds progress even when a response has no tool calls", () => { const feed = buildThreadFeed([ projected(userMessage(), 0), diff --git a/apps/mobile/src/native/composerEditorRevision.test.ts b/apps/mobile/src/native/composerEditorRevision.test.ts index f4888f71577e4..2ddd70e1c3d52 100644 --- a/apps/mobile/src/native/composerEditorRevision.test.ts +++ b/apps/mobile/src/native/composerEditorRevision.test.ts @@ -8,12 +8,6 @@ import { resolveComposerControlledEventCount, } from "./composerEditorRevision"; -describe("acknowledgeComposerNativeEvent", () => { - it("rejects events older than the latest native text revision", () => { - expect(acknowledgeComposerNativeEvent(5, 4)).toBeNull(); - }); -}); - describe("isComposerNativeEcho", () => { const snapshots = [{ eventCount: 3, value: "native", selection: { start: 6, end: 6 } }]; diff --git a/apps/mobile/src/native/workspace-stack-projection.test.ts b/apps/mobile/src/native/workspace-stack-projection.test.ts index 18369a78176ed..9ae5d9a4a7918 100644 --- a/apps/mobile/src/native/workspace-stack-projection.test.ts +++ b/apps/mobile/src/native/workspace-stack-projection.test.ts @@ -88,9 +88,6 @@ describe("native workspace dismissal", () => { expect(nativeWorkspacePopAction(state, settings.key)).toBeNull(); expect(nativeWorkspacePopAction(state, legal.key)).toBeNull(); }); - it("pops a native dismissed file while keeping the conversation and its draft mounted", () => { - expect(nativeWorkspacePopCount(history([home, thread, files]), files.key)).toBe(1); - }); it("removes the descendants when UIKit dismisses their parent conversation", () => { expect(nativeWorkspacePopCount(history([home, thread, files]), thread.key)).toBe(2); @@ -314,6 +311,7 @@ describe("v5 stack handoff", () => { settings, ]); }); + // regression: #17231 keep native screens ordered during stack pops it("retains a multi-page pop in order without changing the active router order", () => { const replacement = { ...thread, key: "replacement" }; const screens = reconcileStackScreens([home, thread, files], [home, replacement]); diff --git a/apps/mobile/src/persistence/mobile-database.test.ts b/apps/mobile/src/persistence/mobile-database.test.ts index 73cf17bafaa64..4e3b792653c8c 100644 --- a/apps/mobile/src/persistence/mobile-database.test.ts +++ b/apps/mobile/src/persistence/mobile-database.test.ts @@ -27,23 +27,6 @@ describe("mobile database legacy cache migration", () => { }); }); - it("preserves the old shell payload for schema decoding after migration", () => { - const payload = JSON.stringify({ - schemaVersion: 1, - environmentId: "environment-1", - snapshotReceivedAt: "2026-07-01T00:00:00.000Z", - snapshot: {}, - }); - - expect(MobileDatabase.decodeLegacyCacheRecord("shell-snapshots", payload)).toEqual({ - environmentId: "environment-1", - kind: "shell", - cacheKey: "snapshot", - schemaVersion: 1, - payload, - }); - }); - it("skips malformed legacy records", () => { expect(MobileDatabase.decodeLegacyCacheRecord("connection-vcs-refs", "{not-json")).toBeNull(); expect( diff --git a/apps/mobile/src/persistence/mobile-storage.test.ts b/apps/mobile/src/persistence/mobile-storage.test.ts index 9bccff93fa73f..363875ade020d 100644 --- a/apps/mobile/src/persistence/mobile-storage.test.ts +++ b/apps/mobile/src/persistence/mobile-storage.test.ts @@ -55,6 +55,7 @@ describe("mobile device identity", () => { }), ); + // regression: #18837 serialize initial device identity creation it.effect("reuses an existing device identity without generating or writing", () => Effect.gen(function* () { values.set(deviceIdKey, "existing-device"); diff --git a/apps/mobile/src/reactCompilerCoverage.test.ts b/apps/mobile/src/reactCompilerCoverage.test.ts index e680a41652e4b..fc6c04aa5833b 100644 --- a/apps/mobile/src/reactCompilerCoverage.test.ts +++ b/apps/mobile/src/reactCompilerCoverage.test.ts @@ -128,11 +128,6 @@ describe("React Compiler coverage", () => { expect(transformed("false")).not.toContain("react/compiler-runtime"); }); - it("app.config.ts turns the compiler on for Metro", () => { - const appConfig = NodeFS.readFileSync(NodePath.join(mobileRoot, "app.config.ts"), "utf8"); - expect(appConfig).toMatch(/experiments:\s*\{[^}]*reactCompiler:\s*true/); - }); - it.each(sourceFiles)("compiles %s", (file) => { const bailouts = compilerBailouts(file); if (file in KNOWN_BAILOUTS) { diff --git a/apps/mobile/src/state/asset-url-state.test.ts b/apps/mobile/src/state/asset-url-state.test.ts index 81751cf31d999..136af9c415572 100644 --- a/apps/mobile/src/state/asset-url-state.test.ts +++ b/apps/mobile/src/state/asset-url-state.test.ts @@ -18,6 +18,7 @@ describe("deriveAssetUrlState", () => { } }); + // regression: #9123 show an error instead of an endless preview spinner it("reports disconnected when the query failed while the environment is down", () => { for (const connectionPhase of ["available", "offline", "reconnecting", "error"] as const) { expect(deriveAssetUrlState({ connectionPhase, shared: { _tag: "Failure" } })).toEqual({ diff --git a/apps/mobile/src/state/composer-attachment-uploads.test.ts b/apps/mobile/src/state/composer-attachment-uploads.test.ts index 226aeb95a7ea8..3ebb0e490e0a0 100644 --- a/apps/mobile/src/state/composer-attachment-uploads.test.ts +++ b/apps/mobile/src/state/composer-attachment-uploads.test.ts @@ -293,7 +293,7 @@ function uploadState() { ]; } -it.each(["decode", "read", "write"] as const)( +it.each(["decode", "write"] as const)( "keeps an accepted upload ready when the draft %s fails", async (fault) => { native.fault = fault; @@ -318,12 +318,10 @@ it.each(["decode", "read", "write"] as const)( }, ); -it.each(["http", "network", "permission"] as const)( +it.each(["http", "permission"] as const)( "keeps a real %s upload failure blocking Send", async (failure) => { if (failure === "http") native.transfer.mockResolvedValueOnce({ status: 503 }); - if (failure === "network") - native.transfer.mockRejectedValueOnce(new Error("Network unavailable")); if (failure === "permission") native.canOperate = false; const queue = worker(); await queue.settled(); diff --git a/apps/mobile/src/state/pending-thread-creation.test.ts b/apps/mobile/src/state/pending-thread-creation.test.ts index 77ede23dfd5b2..67c217081073e 100644 --- a/apps/mobile/src/state/pending-thread-creation.test.ts +++ b/apps/mobile/src/state/pending-thread-creation.test.ts @@ -9,7 +9,6 @@ import { import { describe, expect, it } from "vite-plus/test"; import { - isPendingThreadCreationVisible, resolvePendingThreadCreation, type PendingThreadCreation, } from "./pending-thread-creation"; @@ -139,16 +138,3 @@ describe("resolvePendingThreadCreation", () => { ).toBeNull(); }); }); - -describe("isPendingThreadCreationVisible", () => { - const creationMessageId = String(creation.messageId); - - it("stands down once the delivered prompt lands under the same id", () => { - expect( - isPendingThreadCreationVisible({ - creationMessageId, - loadedMessageIds: ["someone-else", creationMessageId], - }), - ).toBe(false); - }); -}); diff --git a/apps/mobile/src/state/queries.filesystem.test.ts b/apps/mobile/src/state/queries.filesystem.test.ts index 8744d901f9036..8737caccf14bd 100644 --- a/apps/mobile/src/state/queries.filesystem.test.ts +++ b/apps/mobile/src/state/queries.filesystem.test.ts @@ -74,19 +74,3 @@ it("keeps a file search pending until its grant loads, then shows matches", () = isPending: false, }); }); - -it("shows a confirmed denial and a connection failure separately", () => { - state.session = { authenticated: true, scopes: [] }; - expect(useComposerPathSearch(target)).toMatchObject({ - entries: [], - error: "This connection cannot search host files.", - isPending: false, - }); - state.session = null; - state.phase = "offline"; - expect(useComposerPathSearch(target)).toMatchObject({ - entries: [], - error: "This environment is not connected.", - isPending: false, - }); -}); diff --git a/apps/mobile/src/state/thread-outbox.test.ts b/apps/mobile/src/state/thread-outbox.test.ts index 6609e606e86c3..0d08c2af1073b 100644 --- a/apps/mobile/src/state/thread-outbox.test.ts +++ b/apps/mobile/src/state/thread-outbox.test.ts @@ -389,39 +389,6 @@ describe("thread outbox", () => { ).toBe("plan"); }); - it("serializes mutations even when an earlier mutation is slower", async () => { - const registry = AtomRegistry.make(); - const manager = createThreadOutboxManager({ - registry, - storage: { - load: async () => ({ messages: [], errors: [] }), - write: async () => undefined, - remove: async () => undefined, - }, - }); - const order: string[] = []; - let releaseFirst!: () => void; - const firstBlocked = new Promise((resolve) => { - releaseFirst = resolve; - }); - - const first = manager.serialize(async () => { - order.push("first:start"); - await firstBlocked; - order.push("first:end"); - }); - const second = manager.serialize(async () => { - order.push("second"); - }); - - await Promise.resolve(); - expect(order).toEqual(["first:start"]); - releaseFirst(); - await Promise.all([first, second]); - expect(order).toEqual(["first:start", "first:end", "second"]); - registry.dispose(); - }); - it("holds the mutation queue while persisted messages are loading", async () => { const registry = AtomRegistry.make(); const message = queuedMessage({ diff --git a/apps/mobile/src/state/use-composer-drafts.test.ts b/apps/mobile/src/state/use-composer-drafts.test.ts index 6f9292659e925..94dc617a50d49 100644 --- a/apps/mobile/src/state/use-composer-drafts.test.ts +++ b/apps/mobile/src/state/use-composer-drafts.test.ts @@ -191,7 +191,6 @@ import { ensureComposerDraftsLoaded, type ComposerDraft, findNewTaskDraftKeys, - findLocalComposerClipboardAttachment, flushComposerDrafts, getComposerDraftSnapshot, mergeComposerDraftContentState, @@ -616,49 +615,6 @@ describe("mobile composer drafts", () => { ]); }); - it.each(["new-task:draft-1", "pending-task:queued-1"])( - "finds draft-only local clipboard files in %s", - (key) => { - const environmentId = EnvironmentId.make("environment-1"); - const file = { - type: "file" as const, - id: "local-file", - name: "notes.txt", - mimeType: "text/plain", - sizeBytes: 4, - fileUri: "file:///notes.txt", - }; - appAtomRegistry.set(composerDraftsAtom, { - [key]: { - text: "", - attachments: [file], - project: { - environmentId, - projectId: ProjectId.make("project-1"), - createdAt: "2026-01-01T00:00:00.000Z", - }, - }, - }); - appAtomRegistry.set(threadOutboxManager.queuedMessagesByThreadKeyAtom, { - queued: [ - { - environmentId, - threadId: ThreadId.make("thread-1"), - messageId: MessageId.make("queued-1"), - commandId: CommandId.make("command-1"), - text: "Queued", - attachments: [], - createdAt: "2026-01-01T00:00:00.000Z", - }, - ], - }); - expect(findLocalComposerClipboardAttachment(environmentId, file.id)).toEqual(file); - expect( - findLocalComposerClipboardAttachment(EnvironmentId.make("different-environment"), file.id), - ).toBeUndefined(); - }, - ); - it("keeps over-limit recovery context reloadable without losing other drafts", () => { const key = "environment-1:recovery"; const merged = mergeComposerDraftContentState( @@ -1023,48 +979,6 @@ describe("mobile composer drafts", () => { expect(getComposerDraftSnapshot(draftKey).context).toBeUndefined(); }); - // Hydration is one-shot per module instance and the attachment sweep now - // triggers it too, so this test must observe it before any sweep test runs. - it("hydrates generic file attachments from their saved local paths", () => { - const file = { - id: "file-1", - type: "file" as const, - name: "report.pdf", - mimeType: "application/pdf", - sizeBytes: 42, - fileUri: "file:///documents/report.pdf", - }; - - expect( - decodePersistedComposerState({ - schemaVersion: 1, - drafts: { - "environment-1:thread-1": { text: "Review this file", attachments: [file] }, - }, - }).drafts, - ).toEqual({ - "environment-1:thread-1": { - text: "Review this file [report.pdf](t3-context://v1/file/file-1) ", - attachments: [file], - context: { - version: 1, - records: [ - { - version: 1, - contextId: file.id, - kind: "file", - label: file.name, - attachmentId: file.id, - name: file.name, - mimeType: file.mimeType, - sizeBytes: file.sizeBytes, - }, - ], - }, - }, - }); - }); - it("releases videos rejected by the live draft limit and keeps accepted files", async () => { const outboxLoad = vi.spyOn(threadOutboxManager, "load").mockResolvedValue(true); onTestFinished(() => outboxLoad.mockRestore()); @@ -1296,29 +1210,6 @@ describe("mobile composer drafts", () => { expect(getComposerDraftSnapshot(key).attachments).toHaveLength(2); }); - it("cleans up an unreferenced image upload even when there is no local file URI", async () => { - const outboxLoad = vi.spyOn(threadOutboxManager, "load").mockResolvedValue(true); - onTestFinished(() => outboxLoad.mockRestore()); - const environmentId = EnvironmentId.make("environment-1"); - await releaseUnusedComposerAttachmentFiles([ - { - id: "photo", - type: "image", - name: "photo.png", - mimeType: "image/png", - sizeBytes: 3, - dataUrl: "data:image/png;base64,YWJj", - previewUri: "file:///photo.png", - uploadedAttachmentId: "pending-photo", - uploadEnvironmentId: environmentId, - }, - ]); - expect(composerAttachmentCleanupMocks.releaseUploads).toHaveBeenCalledWith(environmentId, [ - "pending-photo", - ]); - expect(composerAttachmentCleanupMocks.remove).not.toHaveBeenCalled(); - }); - it.each(["file", "image"] as const)( "keeps signed-out %s attachments through cleanup and restart, and restores only the owning account", async (type) => { @@ -1861,30 +1752,6 @@ describe("mobile composer drafts", () => { }, }); }); - it("keeps legacy content-only drafts and rejects invalid selector state", () => { - expect( - decodePersistedComposerState({ - schemaVersion: 1, - drafts: { - "environment-1:thread-1": DRAFT, - }, - }).drafts, - ).toEqual({ - "environment-1:thread-1": DRAFT, - }); - - expect(() => - decodePersistedComposerState({ - schemaVersion: 1, - drafts: { - "environment-1:thread-1": { - ...DRAFT, - runtimeMode: "sometimes-safe", - }, - }, - }), - ).toThrow(); - }); it("keeps share-import receipts on otherwise contentless new-task drafts", () => { const receiptDraft: ComposerDraft = { @@ -1926,27 +1793,6 @@ describe("mobile composer drafts", () => { expect(kept[0]).toMatchObject(receiptDraft); }); - it("migrates archived signed-out new-task drafts the same way as live ones", () => { - const decoded = decodePersistedComposerState({ - schemaVersion: 1, - drafts: {}, - cloudAccountId: "account-1", - signedOutDrafts: { - "account-1": { - drafts: { "new-task:environment-1:project-1": { text: "archived", attachments: [] } }, - queuedMessages: [], - }, - }, - }); - const archived = Object.entries(decoded.cloudDrafts.signedOut["account-1"]?.drafts ?? {}); - expect(archived).toHaveLength(1); - expect(archived[0]?.[0]).toMatch(/^new-task:[0-9a-z]+-[0-9a-z]+$/); - expect(archived[0]?.[1]).toMatchObject({ - text: "archived", - project: { environmentId: "environment-1", projectId: "project-1" }, - }); - }); - it("migrates project-keyed new-task drafts to id keys with the project stamped in", () => { const now = "2026-09-05T12:00:00.000Z"; const [key, draft] = migrateLegacyNewTaskDraft( diff --git a/apps/mobile/src/state/use-selected-thread-git-actions.test.ts b/apps/mobile/src/state/use-selected-thread-git-actions.test.ts index 534f5fb4d33b0..8b662c1257d9a 100644 --- a/apps/mobile/src/state/use-selected-thread-git-actions.test.ts +++ b/apps/mobile/src/state/use-selected-thread-git-actions.test.ts @@ -153,37 +153,6 @@ describe("thread Git mutation permissions", () => { }, ); - it.each(["create", "checkout", "commit on new branch"] as const)( - "requires task permission before %s", - async (operation) => { - const actions = useSelectedThreadGitActions(); - if (operation === "create") { - expect(await actions.onCreateSelectedThreadBranch("feature")).toBeNull(); - } - if (operation === "checkout") - expect(await actions.onCheckoutSelectedThreadBranch("feature")).toBeNull(); - if (operation === "commit on new branch") - await actions.onRunSelectedThreadGitAction({ action: "commit", featureBranch: true }); - expect(state.branch).toBe("main"); - expect(state.thread.branch).toBe("main"); - expect(state.commits).toBe(0); - }, - ); - - it("rechecks task permission when a retained menu callback runs", async () => { - state.scopes.add(AuthOrchestrationOperateScope); - const actions = useSelectedThreadGitActions(); - state.scopes.delete(AuthOrchestrationOperateScope); - expect( - await actions.onCreateSelectedThreadWorktree({ - baseBranch: "main", - newBranch: "feature/task", - }), - ).toBeNull(); - expect(state.worktrees).toEqual([]); - expect(state.thread.worktreePath).toBeNull(); - }); - it("keeps ordinary commits and pushes available without task permission", async () => { const actions = useSelectedThreadGitActions(); await actions.onRunSelectedThreadGitAction({ action: "commit" }); diff --git a/apps/mobile/src/state/use-thread-outbox-drain.permissions.test.ts b/apps/mobile/src/state/use-thread-outbox-drain.permissions.test.ts index 74721bb5c9dd2..3bdab5f7a6223 100644 --- a/apps/mobile/src/state/use-thread-outbox-drain.permissions.test.ts +++ b/apps/mobile/src/state/use-thread-outbox-drain.permissions.test.ts @@ -320,8 +320,6 @@ describe("queued task operation access", () => { it.each([ { isCreation: false, accepted: false }, - { isCreation: true, accepted: false }, - { isCreation: false, accepted: true }, { isCreation: true, accepted: true }, ])( "preserves a rejected queue but removes an accepted turn after access loss (new task: $isCreation, accepted: $accepted)", diff --git a/apps/mobile/src/state/use-thread-outbox-drain.test.ts b/apps/mobile/src/state/use-thread-outbox-drain.test.ts index 4d3427bfd9db2..f62686baf2363 100644 --- a/apps/mobile/src/state/use-thread-outbox-drain.test.ts +++ b/apps/mobile/src/state/use-thread-outbox-drain.test.ts @@ -1,7 +1,6 @@ import { acknowledgedThreadMessagesAtom } from "./acknowledged-thread-messages"; import { CommandId, - ComposerContextId, EnvironmentId, MessageId, ProjectId, @@ -277,32 +276,6 @@ describe("thread outbox attachment preparation", () => { expect(remainingMessages()).toEqual([edited]); }); - it("keeps an unchanged queued payload ready after attachment reuse", async () => { - const message = withReusedFileUpload( - queuedMessage({ - messageId: "message-reused-upload-current", - text: "unchanged text", - fileUri: "file:///documents/t3-composer-attachments/current.pdf", - }), - "pending-reused-upload", - ); - harness.prepareTurnAttachments.mockResolvedValueOnce({ - status: "ready", - attachments: [], - draftAttachments: message.attachments, - pendingAttachmentIds: ["pending-reused-upload"], - }); - await harness.manager.enqueue(message); - const revision = harness.manager.revisionOf(message.messageId); - appAtomRegistry.set(editingQueuedMessageIdsAtom, { [message.messageId]: true }); - - await expect(prepareQueuedMessageAttachments(message)).resolves.toMatchObject({ - status: "ready", - persistedMessage: message, - deliveryRevision: revision, - }); - }); - it("uses the known next revision after persisting uploaded references", async () => { const message = queuedMessage({ messageId: "message-new-upload-revision", @@ -616,31 +589,6 @@ describe("thread outbox delivered creation recovery", () => { }); describe("thread outbox recovery rollback", () => { - it("preserves inline context from setup edits when reopening a failed task", async () => { - const message = queuedMessage({ messageId: "failed-context", text: "Original prompt" }); - const sourceKey = `${message.environmentId}:${message.threadId}`; - const targetKey = "new-task:restored-failed-context"; - const record = { - version: 1 as const, - kind: "mention" as const, - contextId: ComposerContextId.make("setup-file"), - label: "Checkout.tsx", - path: "src/Checkout.tsx", - }; - const context = { version: 1 as const, records: [record] }; - const text = "[Checkout.tsx](t3-context://v1/mention/setup-file)"; - appAtomRegistry.set(composerDrafts.composerDraftsAtom, { - [targetKey]: { text: message.text, attachments: [] }, - [sourceKey]: { text, context, attachments: [] }, - }); - await recoverFailedThreadDraft(message); - expect(composerDrafts.getComposerDraftSnapshot(targetKey)).toMatchObject({ - text: `Original prompt\n\n${text}`, - context, - }); - expect(composerDrafts.getComposerDraftSnapshot(sourceKey).context).toBeUndefined(); - }); - it("reopens a rejected task with setup edits and every attachment, even above the send cap", async () => { const message = queuedMessage({ messageId: "failed-setup", text: "Original prompt" }); const sourceKey = `${message.environmentId}:${message.threadId}`; diff --git a/apps/mobile/src/widgets/AgentActivity.test.ts b/apps/mobile/src/widgets/AgentActivity.test.ts deleted file mode 100644 index f42ccd3a069c9..0000000000000 --- a/apps/mobile/src/widgets/AgentActivity.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it, vi } from "vite-plus/test"; - -vi.mock("@expo/ui/swift-ui", () => ({ - HStack: "HStack", - Image: "Image", - Spacer: "Spacer", - Text: "Text", - VStack: "VStack", - ZStack: "ZStack", -})); - -vi.mock("@expo/ui/swift-ui/modifiers", () => ({ - activityBackgroundTint: (value: unknown) => value, - font: (value: unknown) => value, - foregroundStyle: (value: unknown) => value, - frame: (value: unknown) => value, - layoutPriority: (value: unknown) => value, - lineLimit: (value: unknown) => value, - padding: (value: unknown) => value, - resizable: (value: unknown) => value, - widgetURL: (value: unknown) => ({ widgetURL: value }), -})); - -vi.mock("expo-widgets", () => ({ - createLiveActivity: vi.fn((name: string, layout: unknown) => ({ layout, name })), -})); - -import { - AgentActivity, - type AgentActivityProps, - type AgentActivityRowProps, -} from "./AgentActivity"; - -function makeRow(overrides: Partial): AgentActivityRowProps { - return { - environmentId: "env-1", - threadId: "thread-1", - projectTitle: "Project", - threadTitle: "Thread", - modelTitle: "gpt-5.4", - phase: "running", - status: "Working", - updatedAt: "2026-05-25T13:07:00.000Z", - deepLink: "/threads/env-1/thread-1", - ...overrides, - }; -} - -const props = { - title: "T3 Code", - subtitle: "Agent work in progress", - activeCount: 1, - updatedAt: "2026-05-25T13:07:00.000Z", - activities: [], -} satisfies AgentActivityProps; - -const environment = { - colorScheme: "dark", - isLuminanceReduced: false, -} as const; - -describe("AgentActivity widget layout", () => { - it("omits the deep link for unsafe paths and empty aggregates", () => { - expect(JSON.stringify(AgentActivity(props, environment as never))).not.toContain("widgetURL"); - expect( - JSON.stringify( - AgentActivity( - { ...props, activities: [makeRow({ deepLink: "//evil.example" })] }, - environment as never, - ), - ), - ).not.toContain("widgetURL"); - }); -}); diff --git a/apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts b/apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts index 8c69de4670de1..abc4fb0ed0c95 100644 --- a/apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts +++ b/apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts @@ -10,7 +10,6 @@ import { buildSubscriptionUsageSnapshot, createWidgetRefresher, WIDGET_REFRESH_INTERVAL, - subscriptionUsageTimeline, } from "./subscriptionUsageSnapshot"; const checkedAt = "2026-09-05T12:00:00.000Z"; @@ -101,29 +100,6 @@ describe("subscription widget snapshots", () => { expect(snapshot.providers[0]?.name).toBe("Codex"); expect(JSON.stringify(snapshot)).not.toContain("example.com"); }); - it("keeps unavailable quotas distinct from zero usage and omits provider error messages", () => { - const snapshot = buildSubscriptionUsageSnapshot( - presentations([ - provider({ - usageLimits: { - ...limits, - unavailable: { reason: "probeFailed", message: "token secret" }, - }, - }), - ]), - deepLink, - ); - expect(snapshot.providers[0]?.windows).toEqual([]); - expect(JSON.stringify(snapshot)).not.toContain("token secret"); - expect( - buildSubscriptionUsageSnapshot( - presentations([ - provider({ usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 0 }] } }), - ]), - deepLink, - ).providers[0]?.windows[0]?.remaining, - ).toBe(100); - }); it("bounds OS storage and puts the most constrained windows first", () => { const windows = Array.from({ length: 20 }, (_, index) => ({ ...window, @@ -138,16 +114,6 @@ describe("subscription widget snapshots", () => { expect(snapshot.providers[0]?.totalWindows).toBe(20); expect(snapshot.providers[0]?.windows[0]?.remaining).toBe(5); }); - it("marks a malformed check time immediately stale without storing null", () => { - const snapshot = buildSubscriptionUsageSnapshot( - presentations([provider({ usageLimits: { ...limits, checkedAt: "invalid" } })]), - deepLink, - ); - expect(snapshot.checkedAt).toBe(0); - expect(snapshot.providers[0]).toMatchObject({ expiresAt: 0, windows: [] }); - expect(subscriptionUsageTimeline(snapshot, now)).toHaveLength(1); - expect(JSON.stringify(snapshot)).not.toContain("null"); - }); it("uses the freshest copy of an account across environments before pooling", () => { const input = presentations(); input.set(EnvironmentId.make("other"), {