Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
f8ed67b
feat(android): native phone app for Remote Access
BigWebstas Sep 29, 2026
9e3f57c
feat(android): widget card transparency and color
BigWebstas Sep 30, 2026
91bb1ba
feat(remote): add phone diff review and task lifecycle
BigWebstas Sep 30, 2026
5d73b0a
feat(android): merge tasks from the phone with readiness checks
BigWebstas Sep 30, 2026
6381b17
feat(android): ship the desktop's look presets on the phone
BigWebstas Sep 30, 2026
87adeac
Merge branch 'task/lets-look-at-the-android-app-and-8e5115' into feat…
BigWebstas Sep 30, 2026
c619091
fix(remote): explain a phone diff for a task with no branch
BigWebstas Sep 30, 2026
5eca4c8
feat(android): host quick-switch, composer upgrades, connection labels
BigWebstas Oct 1, 2026
5e7eb47
feat(android): add VPN check and wait-to-connect support
BigWebstas Oct 1, 2026
34ac767
test(mermaid): increase render error timeout to avoid flakiness under…
BigWebstas Oct 1, 2026
1fe608c
feat(android): skip waiting for VPN on home Wi-Fi
BigWebstas Oct 3, 2026
fbe7015
ci(android): build, test, and release the phone app
BigWebstas Oct 3, 2026
e6241fb
feat(remote): show agent subscription usage on the phone
BigWebstas Sep 28, 2026
e3882c1
test(ipc): expect renderer-bound usage request channel
BigWebstas Sep 28, 2026
99f520b
feat(git): add commit dialog with stage-all next to merge
BigWebstas Oct 3, 2026
4034675
feat(git): add unstage all to the commit dialog
BigWebstas Oct 4, 2026
24847bf
feat(remote): let paired phones stage, unstage and commit
BigWebstas Oct 4, 2026
63e4deb
feat(android): commit, stage all and unstage all from the phone
BigWebstas Oct 4, 2026
1b0a992
Merge branch 'feat/android-phone-app' into android-app-interfacing
BigWebstas Oct 4, 2026
b35391b
fix(remote): send phones the full terminal history
BigWebstas Oct 4, 2026
5c02518
feat(android): wrap long terminal lines instead of scrolling sideways
BigWebstas Oct 4, 2026
6e9a18c
Merge pull request #13 from BigWebstas/task/agents-terminal-history-d…
BigWebstas Oct 4, 2026
7a42fb8
fix(android): return to the task list after closing a task
BigWebstas Oct 4, 2026
6142119
feat(android): back up and restore phone data
BigWebstas Oct 4, 2026
5576d37
feat(android): widget totals across computers, with icon and status dot
BigWebstas Oct 4, 2026
040057f
fix(android): draw the widget divider with an ImageView
BigWebstas Oct 4, 2026
32d5386
Merge pull request #14 from BigWebstas/task/agents-terminal-history-d…
BigWebstas Oct 4, 2026
c067003
Merge remote-tracking branch 'upstream/main' into feat/android-phone-app
BigWebstas Oct 4, 2026
9077399
Merge remote-tracking branch 'upstream/main' into android-app-interfa…
BigWebstas Oct 4, 2026
296a5d6
Merge remote-tracking branch 'origin/feat/android-phone-app' into and…
BigWebstas Oct 4, 2026
554d5a8
fix(android): spell out "computers" in the widget headline
BigWebstas Oct 4, 2026
811c59c
Merge remote-tracking branch 'origin/feat/android-phone-app' into and…
BigWebstas Oct 4, 2026
91eada8
fix(usage): show Claude extra-usage spend when there are no rate-limi…
BigWebstas Oct 5, 2026
84f7066
fix(android): show Claude extra-usage spend in the usage card and widget
BigWebstas Oct 5, 2026
92e9cdf
fix(android): recognize the home Wi-Fi after reopening the app
BigWebstas Oct 5, 2026
32640ec
refactor(usage): adopt the creditUsage shape from fork/extras
BigWebstas Oct 5, 2026
6e4c7e7
feat(android): show every saved computer's usage on the widget
BigWebstas Oct 5, 2026
cbb1cc2
feat(android): count idle agents in the widget headline
BigWebstas Oct 5, 2026
fe74dd4
fix(tour): keep simple change tours concise
johannesjo Oct 8, 2026
94eb68c
feat(tasks): integrate PR and CI into finish dialog
johannesjo Oct 8, 2026
f0eda2b
Merge branch 'task/tour-feature-should-not-create-many-cards-for-simp…
johannesjo Oct 8, 2026
ce7aeff
fix(github): recognize enterprise pull requests
johannesjo Oct 8, 2026
c256def
Merge branch 'task/fix-enterprise-github-prs-not-being-recognized-003…
johannesjo Oct 8, 2026
83f5631
Merge PR #297 (feat/android-phone-app) into main
johannesjo Oct 8, 2026
81ecb83
fix(remote): keep the desktop's close warnings on a refused close
johannesjo Oct 8, 2026
56912ee
fix(remote): word the phone close dialog from the branch setting
johannesjo Oct 8, 2026
d45cb03
fix(android): drop the commit pane until the desktop serves it
johannesjo Oct 8, 2026
eea2dc7
fix(remote): drop silent phone sockets with a ping heartbeat
johannesjo Oct 8, 2026
0dc63c4
ci(android): run on desktop look changes and harden the release
johannesjo Oct 8, 2026
631b74f
fix(usage): draw the credit meter at 0% used
johannesjo Oct 8, 2026
31f546b
perf(remote): keep long terminal history only while phones can connect
johannesjo Oct 8, 2026
b615788
fix(android): stop polling usage on desktops without the route
johannesjo Oct 8, 2026
8744669
fix(android): keep paired tokens out of unencrypted backups
johannesjo Oct 8, 2026
7a5d1ce
refactor(android): narrow network callback catches
johannesjo Oct 8, 2026
5c0a227
test(pty): cover remote history on running sessions
johannesjo Oct 8, 2026
f06936c
fix(android): survive a revoked notification permission
johannesjo Oct 8, 2026
9b500f0
test(android): cover the missing usage route rule
johannesjo Oct 8, 2026
969332b
feat(android): make agent selectable in new task menu
BigWebstas Oct 8, 2026
bfb6068
feat(android): check GitHub releases for app updates
johannesjo Oct 8, 2026
47d8c54
feat(remote): point Android phones to the native app
johannesjo Oct 8, 2026
b5ee397
docs(android): explain installing and updating the app
johannesjo Oct 8, 2026
cf66dc3
feat(remote): support agent selection for mobile task creation
BigWebstas Oct 8, 2026
af193b8
test(store): allow a cold store import on loaded machines
johannesjo Oct 8, 2026
b5f9de0
Merge branch 'main' into feat/android-phone-app
BigWebstas Oct 8, 2026
b4c5c84
Merge branch 'main' into android-app-interfacing
BigWebstas Oct 8, 2026
6710b5f
Merge remote-tracking branch 'origin/feat/android-phone-app' into fea…
BigWebstas Oct 8, 2026
05bf90b
chore: ignore .idea, CLAUDE.md and GEMINI.md
BigWebstas Oct 12, 2026
e66b671
feat(remote): create projects from the Android app
BigWebstas Oct 12, 2026
78df199
feat(remote): create projects from the Android app
BigWebstas Oct 12, 2026
a431cd5
test(ipc): list the add-project channel in the wiring test
BigWebstas Oct 12, 2026
13b4510
test(ipc): list the add-project channel in the wiring test
BigWebstas Oct 12, 2026
ca4def6
Merge branch 'johannesjo:main' into main
BigWebstas Oct 12, 2026
a1f149f
Merge remote-tracking branch 'origin/main' into feat/android-phone-app
BigWebstas Oct 12, 2026
1434283
Merge remote-tracking branch 'origin/feat/android-phone-app' into and…
BigWebstas Oct 12, 2026
e145ffc
revert(git): remove the commit button and its phone and IPC plumbing
BigWebstas Oct 12, 2026
110dbdf
Merge remote-tracking branch 'origin/main' into android-app-interfacing
BigWebstas Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ docs/*

# Local scratch space (UI preview harnesses, throwaway scripts)
/.tmp/
GEMINI.md
.idea/
CLAUDE.md
16 changes: 14 additions & 2 deletions android/app/src/main/java/com/parallelcode/phone/RemoteClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private const val SLOW_REQUEST_SECONDS = 130L
/** [json] is the error reply's body, for routes that explain a refusal (e.g. close warnings). */
class ApiException(message: String, val status: Int = 0, val json: JSONObject? = null) : IOException(message)

data class MobileProject(val id: String, val name: String, val agentName: String?)
data class MobileProject(val id: String, val name: String, val agentName: String?, val path: String? = null)

/** What another saved computer reported in the last poll, under the name the phone shows for it. */
data class ComputerSnapshot(val label: String, val agents: List<RemoteAgent>, val usage: List<ProviderUsage>)
Expand Down Expand Up @@ -322,10 +322,22 @@ class RemoteClient(
val list = JSONArray(apiRaw("GET", "/api/mobile/projects", null, pairedTokenOrThrow()))
return List(list.length()) { i ->
val p = list.getJSONObject(i)
MobileProject(p.getString("id"), p.getString("name"), p.optString("agentName").ifEmpty { null })
MobileProject(
p.getString("id"),
p.getString("name"),
p.optString("agentName").ifEmpty { null },
p.optString("path").ifEmpty { null },
)
}
}

/** Create a new git project folder on the desktop and add it; returns the project. */
suspend fun createProject(name: String, parentDir: String): MobileProject {
val body = JSONObject().put("name", name).put("parentDir", parentDir)
val p = api("POST", "/api/mobile/projects", body, pairedTokenOrThrow())
return MobileProject(p.getString("id"), p.getString("name"), null, p.optString("path").ifEmpty { null })
}

/**
* Agents and models a paired phone may start tasks with. Empty from a desktop that predates
* the route: it refuses unknown paths for a paired token with 403. Tasks then use its
Expand Down
67 changes: 67 additions & 0 deletions android/app/src/main/java/com/parallelcode/phone/TaskScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
Expand Down Expand Up @@ -74,6 +77,9 @@ fun NewTaskScreen(client: RemoteClient, onDone: () -> Unit, onNeedsPairing: () -
var prompt by rememberSaveable { mutableStateOf("") }
var busy by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
var addingProject by rememberSaveable { mutableStateOf(false) }
var newProjectName by rememberSaveable { mutableStateOf("") }
var newProjectParent by rememberSaveable { mutableStateOf("") }
// Same default title as the phone web UI: the start of the prompt.
val title = name.trim().ifEmpty { prompt.trim().replace(Regex("\\s+"), " ").take(80) }

Expand Down Expand Up @@ -141,6 +147,12 @@ fun NewTaskScreen(client: RemoteClient, onDone: () -> Unit, onNeedsPairing: () -
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
Text(
"PROJECT",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = AppTheme.extra.textMuted,
)
val loaded = projects
when {
loaded == null -> Text("Loading projects…", color = AppTheme.extra.textMuted)
Expand Down Expand Up @@ -194,6 +206,61 @@ fun NewTaskScreen(client: RemoteClient, onDone: () -> Unit, onNeedsPairing: () -
}
}
}
if (addingProject) {
OutlinedTextField(
value = newProjectName,
onValueChange = { newProjectName = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("New project name") },
singleLine = true,
enabled = !busy,
shape = MaterialTheme.shapes.large,
)
OutlinedTextField(
value = newProjectParent,
onValueChange = { newProjectParent = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("Folder on your computer") },
supportingText = { Text("A new git repository is created inside it.") },
singleLine = true,
enabled = !busy,
shape = MaterialTheme.shapes.large,
)
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !busy && newProjectName.isNotBlank() && newProjectParent.isNotBlank(),
shape = MaterialTheme.shapes.large,
onClick = {
busy = true
error = null
scope.launch {
try {
val created = client.createProject(newProjectName.trim(), newProjectParent.trim())
projects = (projects ?: emptyList()) + created
projectId = created.id
newProjectName = ""
addingProject = false
} catch (e: ApiException) {
failed(e)
} finally {
busy = false
}
}
},
) { Text(if (busy) "Creating…" else "Create project", fontWeight = FontWeight.Bold) }
} else if (projects != null) {
TextButton(
onClick = {
if (newProjectParent.isEmpty()) {
// Suggest the folder the existing projects already live in.
newProjectParent = projects?.firstNotNullOfOrNull { it.path }
?.substringBeforeLast('/', "").orEmpty()
}
addingProject = true
},
enabled = !busy,
) { Text("New project…") }
}
OutlinedTextField(
value = prompt,
onValueChange = { prompt = it },
Expand Down
4 changes: 3 additions & 1 deletion electron/ipc/channel-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@
"GetRemoteStatus": "get_remote_status",
"GeneratePairingPin": "generate_pairing_pin",
"Remote_GetProjectsRequest": "remote_get_projects_request",
"Remote_CreateTaskRequest": "remote_create_task_request",
"Remote_GetAgentsRequest": "remote_get_agents_request",
"Remote_CreateTaskRequest": "remote_create_task_request",
"Remote_AddProjectRequest": "remote_add_project_request",
"MCP_ReadMindMapRequest": "mcp_read_mindmap_request",
"MCP_ReadReasoningRequest": "mcp_read_reasoning_request",
"MCP_UpdateReasoningRequest": "mcp_update_reasoning_request",
Expand All @@ -106,6 +107,7 @@
"Remote_GetDiffRequest": "remote_get_diff_request",
"Remote_GetMergeReadinessRequest": "remote_get_merge_readiness_request",
"Remote_MergeTaskRequest": "remote_merge_task_request",
"Remote_GetUsageRequest": "remote_get_usage_request",
"Remote_GetFixCiPromptRequest": "remote_get_fix_ci_prompt_request",
"Remote_SendFixCiPromptRequest": "remote_send_fix_ci_prompt_request",
"Remote_UpdateTaskStatus": "remote_update_task_status",
Expand Down
2 changes: 2 additions & 0 deletions electron/ipc/register-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const NOT_HANDLED_HERE: readonly string[] = [
IPC.NotificationFailed,
IPC.PlanContent,
IPC.PrChecksUpdate,
IPC.Remote_AddProjectRequest,
IPC.Remote_CloseTaskRequest,
IPC.Remote_CreateTaskRequest,
IPC.Remote_GetAgentsRequest,
Expand All @@ -97,6 +98,7 @@ const NOT_HANDLED_HERE: readonly string[] = [
IPC.Remote_GetFixCiPromptRequest,
IPC.Remote_SendFixCiPromptRequest,
IPC.Remote_MergeTaskRequest,
IPC.Remote_GetUsageRequest,
IPC.Remote_SetNotesRequest,
IPC.SuperProductivityOpenTaskRequested,
IPC.UpdateStatusChanged,
Expand Down
10 changes: 10 additions & 0 deletions electron/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { readCoverageSummary } from './coverage.js';
import { loadEslintQualityFindings } from './eslint-quality-findings.js';
import { buildVerifyEnv, validateVerifyCommand, verificationRunner } from './verify.js';
import { scanEvidence } from './evidence-scan.js';
import { createProjectFolder } from '../remote/project-create.js';
import {
startRemoteServer,
getMCPLogs,
Expand All @@ -88,6 +89,7 @@ import type {
RemoteTaskContext,
RemoteTaskDiff,
} from '../remote/protocol.js';
import type { UsageProvider, UsageState } from './shared-types.js';
import { atomicWriteFileSync } from '../mcp/atomic.js';
import { getUserDataDir } from '../user-data-dir.js';
import {
Expand Down Expand Up @@ -1801,6 +1803,13 @@ export function registerAllHandlers(win: BrowserWindow): void {
getAgentChoices: () => callRenderer<RemoteAgentChoice[]>(IPC.Remote_GetAgentsRequest, {}),
createTaskFromMobile: (req: MobileTaskRequest) =>
callRenderer<{ taskId: string }>(IPC.Remote_CreateTaskRequest, req),
createProjectFromMobile: async (req: { parentDir: string; name: string }) => {
const projectPath = await createProjectFolder(req.parentDir, req.name);
return callRenderer<RemoteProject>(IPC.Remote_AddProjectRequest, {
name: path.basename(projectPath),
path: projectPath,
});
},
getTaskNotes: (taskId: string) =>
callRenderer<{ notes: string }>(IPC.Remote_GetNotesRequest, { taskId }).then((r) => r.notes),
setTaskNotes: (taskId: string, notes: string) =>
Expand All @@ -1821,6 +1830,7 @@ export function registerAllHandlers(win: BrowserWindow): void {
),
mergeTaskFromMobile: (req: { taskId: string; squash: boolean; cleanup: boolean }) =>
callRenderer<{ ok: boolean }>(IPC.Remote_MergeTaskRequest, req).then(() => {}),
getUsage: () => callRenderer<Record<UsageProvider, UsageState>>(IPC.Remote_GetUsageRequest, {}),
getTaskAttention: (taskId: string): RemoteAttentionState => taskAttention.get(taskId) ?? 'idle',
getTaskContext: (taskId: string) => taskContext.get(taskId),
getCollapsedTaskIds: (): string[] => {
Expand Down
12 changes: 12 additions & 0 deletions electron/ipc/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,18 @@ export type UsageResult =
/** Transient failure — the renderer keeps its last good snapshot. */
| { status: 'error'; message: string };

/** The renderer's view of one provider's usage, also served to the phone. */
export interface UsageState {
fiveHour: UsageWindow | null;
sevenDay: UsageWindow | null;
creditUsage?: CreditUsage | null;
/** When the current windows were fetched; null until the first success. */
fetchedAt: number | null;
/** `unavailable` means no subscription login — the bar hides. `error` keeps the last snapshot. */
status: 'idle' | 'ok' | 'error' | 'unavailable';
error: string | null;
}

/** One OS process in the resources panel. */
export interface ResourceProcess {
pid: number;
Expand Down
4 changes: 3 additions & 1 deletion electron/preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ const ALLOWED_CHANNELS = new Set([
'get_remote_status',
'generate_pairing_pin',
'remote_get_projects_request',
'remote_create_task_request',
'remote_get_agents_request',
'remote_create_task_request',
'remote_add_project_request',
'mcp_read_mindmap_request',
'mcp_read_reasoning_request',
'mcp_update_reasoning_request',
Expand All @@ -113,6 +114,7 @@ const ALLOWED_CHANNELS = new Set([
'remote_get_diff_request',
'remote_get_merge_readiness_request',
'remote_merge_task_request',
'remote_get_usage_request',
'remote_get_fix_ci_prompt_request',
'remote_send_fix_ci_prompt_request',
'remote_update_task_status',
Expand Down
38 changes: 38 additions & 0 deletions electron/remote/notes-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,41 @@ describe('notes route without a renderer bridge', () => {
expect(res.status).toBe(503);
});
});

describe('GET /api/mobile/usage', () => {
const usage = {
claude: {
fiveHour: { usedPercent: 40, resetsAt: null },
sevenDay: null,
fetchedAt: 1,
status: 'ok' as const,
error: null,
},
codex: {
fiveHour: null,
sevenDay: null,
fetchedAt: null,
status: 'idle' as const,
error: null,
},
};
const getUsage = vi.fn(async () => usage);
beforeEach(() => start({ getUsage }));

it('returns the desktop snapshot for mobile and paired tokens', async () => {
for (const token of [mobileToken, await pair()]) {
const res = await request('GET', '/api/mobile/usage', { token });
expect(res.status).toBe(200);
expect(res.json).toEqual(usage);
}
});

it('rejects agent tokens and non-GET methods', async () => {
getUsage.mockClear();
for (const token of [coordinatorToken, subtaskToken]) {
expect((await request('GET', '/api/mobile/usage', { token })).status).toBe(403);
}
expect((await request('POST', '/api/mobile/usage', { token: mobileToken })).status).toBe(405);
expect(getUsage).not.toHaveBeenCalled();
});
});
3 changes: 3 additions & 0 deletions electron/remote/pairing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async function startServer(enableRemembered = true) {
beforeEach(async () => {
createTaskFromMobile.mockClear();
getProjects.mockClear();
getAgentChoices.mockClear();
credentialsDir = mkdtempSync(join(tmpdir(), 'phone-pairing-'));
await startServer();
});
Expand Down Expand Up @@ -316,13 +317,15 @@ describe('paired-mobile routes', () => {
projectId: 'proj-1',
name: 'Fix bug',
prompt: 'Investigate the crash',
agentId: 'claude',
});
expect(res.status).toBe(201);
expect(await res.json()).toEqual({ taskId: 'task-123' });
expect(createTaskFromMobile).toHaveBeenCalledWith({
projectId: 'proj-1',
name: 'Fix bug',
prompt: 'Investigate the crash',
agentId: 'claude',
});
});

Expand Down
30 changes: 30 additions & 0 deletions electron/remote/project-create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createProjectFolder } from './project-create.js';

describe('createProjectFolder', () => {
let parent: string;
beforeEach(() => {
parent = fs.mkdtempSync(path.join(os.tmpdir(), 'pc-newproj-'));
});
afterEach(() => {
fs.rmSync(parent, { recursive: true, force: true });
});

it('creates a git repository in a new folder', async () => {
const created = await createProjectFolder(parent, ' demo ');
expect(created).toBe(path.join(parent, 'demo'));
expect(fs.existsSync(path.join(created, '.git'))).toBe(true);
});

it('refuses an existing folder, a missing parent and path-like names', async () => {
fs.mkdirSync(path.join(parent, 'taken'));
await expect(createProjectFolder(parent, 'taken')).rejects.toThrow('already exists');
await expect(createProjectFolder(path.join(parent, 'nope'), 'x')).rejects.toThrow('parent');
await expect(createProjectFolder('relative/dir', 'x')).rejects.toThrow('absolute');
for (const bad of ['', '..', 'a/b', 'a\\b', 'a\nb'])
await expect(createProjectFolder(parent, bad)).rejects.toThrow();
});
});
Loading
Loading