mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* fix(remote-terminal): keep a reconnected pane's size after an SSH outage (P1-4) (#26625) * fix(remote-terminal): keep a reconnected pane's grid when its stale stream is reaped (P1-4) The host's width floor only handed ownership to viewers that had claimed since subscribing. A reconnecting desktop pane re-subscribes without claiming, so when the heartbeat later reaped the dead socket that owned the floor, the host reclaimed the PTY to its spawn size (120x40) and held the pane there. The owner's same-client replacement stream now inherits the floor, and a pane whose stream was reaped before it reconnected resumes ownership on re-register unless the host or another viewer claimed since. * fix(remote-terminal): host typing during an outage revokes the reaped owner's resume Host input reaches the PTY without a renderer viewport claim once the host has reclaimed its grid, so the orphaned-owner entitlement survived and a passive reconnect resized the host pane back to the remote grid. The host write path now clears that entitlement on driving input (query replies, focus reports and launch writes excluded). * test(pty): stub the runtime's host-input note in write-path handler tests --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(remote): an oversized RPC reply fails that request instead of closing the host connection (#26624) Expanding a folder with ~80k entries on a managed SSH host produced a files.readDir reply over the 4 MiB encrypted-reply cap. The channel treated that as a transport fault and closed the whole control socket (1013), and the explorer rendered the failed folder as empty. - The WebSocket RPC transport now answers an over-cap reply with a reply_too_large error for that request id, so the channel never sees it. - files.readDir checks remote listings against the request's content budget and names the entry count. - The explorer shows a subfolder's read error on its expanded row. Finding: HH-2 (Phase 3.7 hostile-host). Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): let a slow host finish installing the pinned Node runtime (HH-3) (#26631) * fix(ssh): let a slow host finish installing the pinned Node runtime The POSIX promote (extract, hash, self-test and publish the ~110 MiB node binary) ran under the 30 s default exec timeout. On slow home storage it timed out, the remote work kept running, and the runtime store lock it held stayed until the 20-minute stale rule, so relay rung A queued behind it past its own deploy bound. Give both platforms the 300 s promote bound and have the host release the store lock itself when the promote exits, with owner-checked releases so a late release never removes a lock someone else took. (HH-3) * fix(ssh): let the host release the store lock after a slow under-lock re-hash The under-lock re-check of a sibling-published runtime hashed the ~110 MiB executable as its own command under the 30 s exec default and outside the host-side lock release. On slow storage it timed out, the client correctly kept the lock, and nothing on the host freed it until the 20-minute stale rule, so later installers ran out their deploy bound. Run the re-hash and the conditional promote as one host command wrapped in the host release, and give every full-runtime hash, including the initial probe, the slow-storage bound. (HH-3) * test(ssh): count the combined under-lock re-probe and promote as one host command --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): shell startup output on stdout no longer breaks SSH connect (HH-1) (#26617) * fix(ssh): fence captured exec stdout so shell rc output cannot break connect sshd runs the user's login shell before Orca's /bin/sh wrapper, so any startup-file stdout (fish/tcsh/zsh/bash echo, or a bare terminal-title escape) was glued onto every captured answer. The home probe read "banner\n/home/u" and connect failed with "Remote home is not a valid path". execCommand now wraps wrapped POSIX commands in a per-call nonce fence and returns only the payload between the markers; the SSH folder browser gets the same fence. The fence is the existing WSL captured-login-shell fence, moved to src/shared/posix-stdout-fence.ts so both transports share it. Finding: HH-1 (phase 3.7 hostile-host). Refs #8324. * fix(ssh): fence the login-shell Node probe and fall back from a noisy SFTP handshake The login-shell 'command -v node' probe ran unwrapped, so output from sshd's shell and the nested login profile became the Node path. It now prints a one-line fence every login shell parses, inside that shell. An external sftp-server is started through the user's shell, so startup output corrupts the SFTP handshake. ssh2 reports that parse failure only before the session is ready, so SshConnection.sftp tags it, and relay install transfers treat it as SFTP unavailable and stream over exec stdin. Finding: HH-1 follow-up (phase 3.7 hostile-host). Refs #8324. * fix(ssh): keep the stdout fence marker out of the command text The runtime-store inventory lists processes with ps, which prints its own script, so a literal marker in the script was read as the fence and the store GC skipped every pass. printf now joins the marker from two halves. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(orcad): finish an abandoned update on connect, and refresh renderers after a main-side re-pair (P1-A, P1-C) (#26635) * fix(orcad): finish an update or rollback its client abandoned, and tell renderers about main-side re-pairs P1-A: a wake that finds the activation fence now runs crash recovery, which takes only a stale, previous-boot or exited-own fence. Recovery of an activation interrupted after the candidate launched resumes it: the candidate commits if it passes the readiness gate, else the activation is undone as before. P1-C: invalidating a still-linked environment's transport restarts its status owner, and a renderer that sees status under a newer pairing revision re-reads the catalog before applying it. * fix(orcad): a resumed update keeps the app version that started it Recovery that commits an abandoned candidate stamped no activeAppVersion, so an older desktop read the host as pre-tracking and downgraded it over state the newer build had migrated. The activate journal now records the initiating app version; recovery stamps it, and a journal too old to carry it is undone instead of committed. * test(runtime-env): answer nothing for the status restart a re-pair now triggers * fix(orcad): recovery leaves running an update server a too-old journal cannot commit A journal without candidateAppVersion was sent to undo, which stopped a healthy candidate that had already migrated state and then refused to restart the incumbent, leaving the host serving nothing. Refuse instead, keeping the fence and the candidate, until the operator accepts Restore. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(terminal): a remote recovery never splices stale history above the new screen (P2-5, P2-7) (#26647) * fix(terminal): a remote recovery never splices stale history above the new screen A pushed remote snapshot (overflow recovery, seq-gap resync, first attach) is sent because the client's stream broke, so the pane's own history ends before that image. Since #26319 a screen-only image kept that history, splicing a stale block directly above the final screen with a silent gap of up to ~270k lines (P2-5), and a first attach to an older host left silent holes (P2-7). - Host: the ACK-overflow recovery serializes up to 1000 history rows. - Client: the seq-gap resync asks for the same 1000 rows. - Client: a pushed snapshot always replaces the pane's scrollback; the keep-local-scrollback path is removed. A screen-only image from an older host now shows only its screen, which is lossy but never misleading. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(terminal): a pushed image replaces the normal buffer over a live alt screen too Over a pane already on the alternate screen, an image that also ended on alt repainted only its alt frame and kept the pane's normal buffer, inferring the two had frozen together. Inside the gap the TUI can exit, the shell print and a second TUI start, so once that TUI exited the pane showed its stale history where the host had the missed output. A pushed image now always rebuilds the normal buffer and history from the host before entering alt itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(terminal): bound recovery history by link slack and keep pre-TUI scrollback under screen-only images A recovery that carries history now goes out only when the stream's ACK window has drained; under a sustained flood recoveries stay screen-only and the owed history follows once the client catches up, so snapshot bytes no longer outgrow the client's intake. A pushed image without history (scrollbackRows absent or 0) over a live alt screen repaints only the alt frame again, so a reconnect to an older host no longer wipes the pane's pre-TUI scrollback. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(source-control): Stage All stages every change when the listing hit the 1,000 cap (HH-4) (#26618) * fix(source-control): Stage All stages every change when the listing hit the 1,000 cap The status listing stops at 1,000 rows, but Stage All built its path list from those rows, so it staged 1,000 of 20,000 changes and the next commit was silently partial. When the listing is capped, Stage All (and the Changes header Stage all) now asks the host to stage the whole worktree via an optional scope on git.bulkStage; unresolved conflicts stay unstaged. Older hosts ignore the field and stage the listed paths. Fixes HH-4. * fix(source-control): whole-worktree Stage All needs a host receipt, skips shared links, and reaches mobile - Scoped stage sends no paths and requires a stagedScope receipt, so an older runtime or relay stages nothing and the client reports it instead of a silent partial stage. - Scoped 'all' skips Orca's shared symlinks (configured and really a link), matching the status listing. - The capped-listing banner offers Stage All, reachable when the visible prefix is entirely staged. - Mobile reads didHitLimit and uses the same scoped stage and receipt. * test(source-control): type the Stage All test stubs so lint and the mobile test typecheck pass * fix(source-control): match Shared Paths saved as ./name or name/ against git's spelling The Shared Paths setting keeps entries as typed, but git prints worktree paths without a leading ./ or trailing /. Stage All's conflict path compared the raw spelling to git's untracked list and staged the shared node_modules link, and an exclude pathspec ending in / made git refuse the add after tracked changes were already staged. Status listing had the same mismatch. getSafeRelativePath now returns git's spelling, so every caller gets it, including saved settings. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) (#26628) * fix(ssh): redeploy a server another desktop stopped; let a dead managed host be forgotten P1-D: a connect whose linked server the host reports as not activated (another desktop stopped it) now goes on to the update check, which redeploys it in place. P1-E: add Forget (managedServer.forget, orca environment forget, Settings row) that runs Stop's local unlink without contacting the host; the verdict stays unverifiable. * test(ssh): type the forget test's hoisted store without an assertion * fix(ssh): never downgrade a stopped host; redeploy it mid-session and retry after a failure A stop now keeps the stopped build's Orca version, so an older desktop's reconnect skips as host-newer instead of starting an older server on a store a newer build migrated. A stop recorded without that version is not guessed at. A stopped host retries the redeploy on each connect instead of the once-per-version failure suppression, and a runtime call that finds the server stopped mid-session redeploys it before the call. * fix(ssh): rebuild the managed forward after redeploying a stopped host A redeploy can bind a different port than the reused forward targets. * fix(ssh): keep stop journals readable by older desktops and recheck version policy under the fence - The stop journal's recordAfter is the core deactivated record older peers compute; the committed record keeps activeAppVersion (which released planners already honour) and rolledBackFrom, rebuilt from recordBefore on recovery. - Auto-update re-applies its version policy to the record read under the host fence, and a census counted against another incumbent is treated as unverifiable. * fix(ssh): file a dispatched stop where older desktops cannot commit it, so their recovery never drops the version guard * fix(ssh): refuse a fresh deploy of an older orcad onto a host a newer desktop stopped The unlinked deploy path never consulted the stopped record's activeAppVersion, so a desktop without a link to the host could activate its older build over state a newer build migrated. Both the auto-update planner and the fresh deploy now apply one downgrade rule under the fence. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): redeploy a managed server a pre-version Stop left; let a forced deploy run over a newer one (#26766) A stopped activation record without activeAppVersion is what every Stop on main writes, so refusing it stranded those hosts behind an unforceable policy refusal. Only a provably newer host is now refused; a forced fresh deploy runs this version anyway, and the refusal names both versions. Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage (P1-1, P1-2, P1-3) (#26630) * fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage Remote pane input was fire-and-forget: keys handed to a socket that had silently died were gone, keys still in the 8 ms debounce at detection were cleared, and held keys were discarded when the auto-recovery window ran out. - New negotiated `inputAck` capability: the client names an input session and sequences Input frames (existing frame header `seq`); the host records the last applied sequence per (PTY, session) across connections, writes each sequence once and acks it with new opcode InputAck (18), sent only to clients that advertised the capability. - The pane keeps sent-but-unacked input and replays it to the same terminal on reattach; the host dedupes. Old hosts keep current behavior. - Debounced and claim-queued input at detection is held, not cleared. - Held input survives the 'disconnected' phase; the hold coalesces keys so minutes of typing do not hit the per-entry cap. - Banner copy no longer claims a one-minute retry window. Fixes P1-1, P1-2, P1-3 (phase 3.7 network findings). * fix(remote-terminal): never replay into a restarted runtime; keep Ctrl+C and validating paste in order - Host publishes inputLedgerId with inputAck; the client drops unacknowledged input sent to a different ledger (runtime restart with the same PTY) instead of running it twice. - A replayed duplicate is acked only after the original write settles. - Accepted input (Ctrl+C, Escape, paste) rides the sequenced stream and resolves on the host ack. - Keys typed while held queue behind a paste still in size validation. * fix(remote-terminal): parse subscribed capabilities into a typed shape instead of Reflect.get * fix(remote-terminal): ack only applied input; withdraw writes their caller gave up on - Host ledger records each write's outcome: a refused write is rolled back and never acked, so the client's replay writes it; nothing queued behind it runs or is acked. A write that threw is acked as delivery-unknown, so the client reports it failed and never replays it. - Paste and file-drop timeouts abort their accepted write: a copy still held for an outage is withdrawn, and a journaled copy is left out of the replay. - A batch of sequenced input keeps one endpoint and ledger, so a send that retires the stream mid-batch no longer discards earlier unacked input. * fix(remote-terminal): host writes sequenced input strictly in order and asks the live stream to resend after a refusal - The ledger admits only the next sequence; a refusal or gap answers with an InputAck resend request instead of WriteUnavailable, so the pane keeps its journal and resends on the same stream (no remount, no carrier bookkeeping). - Sequenced input uses the provider's settled write; an unverifiable handoff is delivery-unknown, not refused. - Replays fill given-up slots with empty input so the host can continue; the client remounts only after every resend backoff is refused. - Typing batched ahead of a paste goes out as its own sequences, so cancelling the paste keeps it. * refactor(remote-terminal): deliver sequenced input with standard go-back-N Replace the hand-built "block on refusal" bookkeeping with go-back-N: - Host: per (PTY, input session) the ledger keeps only appliedSeq, the highest contiguous confirmed write. Frames run one at a time in arrival order: a duplicate is re-acked, the next sequence is written and acked on confirmed success, and a refusal or gap is answered with a resend request carrying appliedSeq. No blocked state; every stream is equal. A settlement that proves no byte left is a refusal; one that may have written advances as delivery-unknown and is never retried. - Client: one journal keeps unacked input in order and resends from appliedSeq + 1 on a resend request (with backoff), on a new stream, or when acks stall. Refusals never remount the pane. Withdrawn pastes, stale query replies and over-budget bytes resend as empty slots. - The recovery hold pauses (keeps the rest) when a release is cut short by another outage, and keeps delivering typing after an accepted write a sequenced stream withdrew or could not confirm. - Add a seeded model-based test that drives the real pane transport, multiplexer and journal against the real host dispatcher and ledger through 2,000 random fault schedules. * test(remote-terminal): cover the refusal remount on a stream without sequenced input A sequenced stream now resends refused input on the same stream instead of remounting, so the end-to-end remount check negotiates no inputAck, as an older peer does. * fix(remote-terminal): escalate persistent input refusals, drop input once recovery gives up, and never report a lost unknown ack as applied - The pane falls back to its remount path when the host keeps refusing input past the resend backoff ladder. - Held and journaled input is discarded when auto-recovery gives up, and input typed under the disconnected banner is not held. - The host ledger answers a duplicate for its own sequence and remembers delivery-unknown outcomes; the client settles a write as applied only from its own ack. - The input model now requires a write reported applied to have reached the PTY whole, and covers a refusal that never clears. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Claude <noreply@anthropic.com> * fix(remote-terminal): keep input typed past the auto-recovery window for the same terminal, within a bounded grace (P1-3) (#26832) The last round of #26630 discarded held and journaled input as soon as auto-recovery gave up, and refused input under the disconnected banner. On a real host a 200 s outage lost 102 of 120 keystrokes, although the pane reattached the same terminal on its own seconds later. Input now stays held for the same handle and incarnation while the pane is disconnected, for up to 5 minutes after it gave up. Past that grace it is discarded, and typing into the dead pane is refused, so stale keys never run at an arbitrary later reconnect. The grace is a timestamp checked when input moves, so a latched pane stays free of timers. Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
805 lines
26 KiB
TypeScript
805 lines
26 KiB
TypeScript
import { mkdtempSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { OrcaRuntimeRpcServer } from './runtime-rpc'
|
|
import { remoteRpcContentBudget } from '../../shared/remote-rpc-content-budget'
|
|
import { DeviceRegistry } from './device-registry'
|
|
import { createMobileRpcSurfaceRuntime } from './runtime-rpc-mobile-method-allowlist-fixtures'
|
|
|
|
describe('OrcaRuntimeRpcServer', () => {
|
|
it('limits mobile-scoped WebSocket tokens to the mobile RPC surface', async () => {
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
|
|
const { runtime, mocks, expectedCodexResetScope } = createMobileRpcSurfaceRuntime()
|
|
const server = new OrcaRuntimeRpcServer({ runtime, userDataPath, enableWebSocket: false })
|
|
server['deviceRegistry'] = new DeviceRegistry(userDataPath)
|
|
const mobile = server['deviceRegistry']!.addDevice('phone', 'mobile')
|
|
const replies: Record<string, unknown>[] = []
|
|
const dispatch = async (request: Record<string, unknown>): Promise<void> => {
|
|
await server['handleWebSocketMessage'](
|
|
JSON.stringify(request),
|
|
(response) => replies.push(JSON.parse(response) as Record<string, unknown>),
|
|
() => {}
|
|
)
|
|
}
|
|
|
|
await dispatch({
|
|
id: 'req_forbidden',
|
|
// files.delete is a real registered RPC intentionally kept off the
|
|
// mobile allowlist — mobile clients must never delete host files.
|
|
method: 'files.delete',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_allowed',
|
|
method: 'status.get',
|
|
deviceToken: mobile.token
|
|
})
|
|
await dispatch({
|
|
id: 'req_settings_get',
|
|
method: 'settings.get',
|
|
deviceToken: mobile.token
|
|
})
|
|
await dispatch({
|
|
id: 'req_settings_update',
|
|
method: 'settings.update',
|
|
deviceToken: mobile.token,
|
|
params: { defaultTaskSource: 'linear' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_projects',
|
|
method: 'github.project.listAccessible',
|
|
deviceToken: mobile.token,
|
|
params: {}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_issue_types',
|
|
method: 'github.project.listIssueTypesBySlug',
|
|
deviceToken: mobile.token,
|
|
params: { owner: 'stablyai', repo: 'orca' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_labels',
|
|
method: 'github.project.listLabelsBySlug',
|
|
deviceToken: mobile.token,
|
|
params: { owner: 'stablyai', repo: 'orca' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_assignees',
|
|
method: 'github.project.listAssignableUsersBySlug',
|
|
deviceToken: mobile.token,
|
|
params: { owner: 'stablyai', repo: 'orca', seedLogins: ['alex'] }
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_update_issue',
|
|
method: 'github.project.updateIssueBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
updates: { title: 'New title' }
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_update_issue_type',
|
|
method: 'github.project.updateIssueTypeBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
issueTypeId: 'type-1'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_update_field',
|
|
method: 'github.project.updateItemField',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
projectId: 'project-1',
|
|
itemId: 'item-1',
|
|
fieldId: 'field-1',
|
|
value: { kind: 'text', text: 'Ready' }
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_clear_field',
|
|
method: 'github.project.clearItemField',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
projectId: 'project-1',
|
|
itemId: 'item-1',
|
|
fieldId: 'field-1'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_update_pr',
|
|
method: 'github.project.updatePullRequestBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 456,
|
|
updates: { state: 'closed' }
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_add_comment',
|
|
method: 'github.project.addIssueCommentBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
body: 'done'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_update_comment',
|
|
method: 'github.project.updateIssueCommentBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
commentId: 101,
|
|
body: 'edited'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_project_delete_comment',
|
|
method: 'github.project.deleteIssueCommentBySlug',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
commentId: 101
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_update_issue',
|
|
method: 'github.updateIssue',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
number: 123,
|
|
updates: { title: 'New title', addLabels: ['bug'] }
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_labels',
|
|
method: 'github.listLabels',
|
|
deviceToken: mobile.token,
|
|
params: { repo: 'id:repo-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_assignees',
|
|
method: 'github.listAssignableUsers',
|
|
deviceToken: mobile.token,
|
|
params: { repo: 'id:repo-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_add_comment',
|
|
method: 'github.addIssueComment',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
number: 123,
|
|
body: 'done'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_add_review_comment',
|
|
method: 'github.addPRReviewComment',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
commitId: 'abc123',
|
|
path: 'src/app.ts',
|
|
line: 10,
|
|
body: 'please fix'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_reply_review_comment',
|
|
method: 'github.addPRReviewCommentReply',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
commentId: 99,
|
|
body: 'fixed',
|
|
threadId: 'thread-1',
|
|
path: 'src/app.ts',
|
|
line: 10
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_pr_file_contents',
|
|
method: 'github.prFileContents',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
path: 'src/app.ts',
|
|
status: 'modified',
|
|
headSha: 'abc123',
|
|
baseSha: 'def456'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_rerun_checks',
|
|
method: 'github.rerunPRChecks',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
headSha: 'abc123',
|
|
failedOnly: true
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_resolve_thread',
|
|
method: 'github.resolveReviewThread',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
threadId: 'thread-1',
|
|
resolve: true
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_file_viewed',
|
|
method: 'github.setPRFileViewed',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
pullRequestId: 'PR_kw',
|
|
path: 'src/app.ts',
|
|
viewed: true
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_request_reviewers',
|
|
method: 'github.requestPRReviewers',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
reviewers: ['alex']
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_github_merge_pr',
|
|
method: 'github.mergePR',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
prNumber: 456,
|
|
method: 'squash'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_gitlab_add_issue_comment',
|
|
method: 'gitlab.addIssueComment',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
number: 123,
|
|
body: 'done'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_gitlab_add_mr_comment',
|
|
method: 'gitlab.addMRComment',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
iid: 456,
|
|
body: 'ship it'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_gitlab_resolve_mr_discussion',
|
|
method: 'gitlab.resolveMRDiscussion',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
iid: 456,
|
|
discussionId: 'discussion-1',
|
|
resolved: true
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_gitlab_merge_mr',
|
|
method: 'gitlab.mergeMR',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
repo: 'id:repo-1',
|
|
iid: 456,
|
|
method: 'merge'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_linear_search',
|
|
method: 'linear.searchIssues',
|
|
deviceToken: mobile.token,
|
|
params: { query: 'auth', limit: 10, workspaceId: 'workspace-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_linear_select_workspace',
|
|
method: 'linear.selectWorkspace',
|
|
deviceToken: mobile.token,
|
|
params: { workspaceId: 'workspace-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_linear_team_labels',
|
|
method: 'linear.teamLabels',
|
|
deviceToken: mobile.token,
|
|
params: { teamId: 'team-1', workspaceId: 'workspace-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_linear_team_members',
|
|
method: 'linear.teamMembers',
|
|
deviceToken: mobile.token,
|
|
params: { teamId: 'team-1', workspaceId: 'workspace-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_linear_add_comment',
|
|
method: 'linear.addIssueComment',
|
|
deviceToken: mobile.token,
|
|
params: { issueId: 'issue-1', workspaceId: 'workspace-1', body: 'done' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_status',
|
|
method: 'git.status',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_push',
|
|
method: 'git.push',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', publish: true }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_upstream',
|
|
method: 'git.upstreamStatus',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_rebase_from_base',
|
|
method: 'git.rebaseFromBase',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', baseRef: 'origin/main' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_bulk_stage',
|
|
method: 'git.bulkStage',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', filePaths: ['a.ts', 'b.ts'] }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_abort_merge',
|
|
method: 'git.abortMerge',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_abort_rebase',
|
|
method: 'git.abortRebase',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_bulk_unstage',
|
|
method: 'git.bulkUnstage',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', filePaths: ['c.ts'] }
|
|
})
|
|
await dispatch({
|
|
id: 'req_select_claude',
|
|
method: 'accounts.selectClaude',
|
|
deviceToken: mobile.token,
|
|
params: { accountId: 'claude-account' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_select_codex',
|
|
method: 'accounts.selectCodex',
|
|
deviceToken: mobile.token,
|
|
params: { accountId: null }
|
|
})
|
|
await dispatch({
|
|
id: 'req_consume_codex_reset',
|
|
method: 'accounts.consumeCodexResetCredit',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
idempotencyKey: '11111111-1111-4111-8111-111111111111',
|
|
expectedScope: expectedCodexResetScope
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_remove_claude',
|
|
method: 'accounts.removeClaude',
|
|
deviceToken: mobile.token,
|
|
params: { accountId: 'claude-account' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_terminal_read',
|
|
method: 'terminal.read',
|
|
deviceToken: mobile.token,
|
|
params: { terminal: 'term-1' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_files_open_diff',
|
|
method: 'files.openDiff',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', relativePath: 'docs/readme.md', staged: true }
|
|
})
|
|
await dispatch({
|
|
id: 'req_git_diff',
|
|
method: 'git.diff',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', filePath: 'docs/readme.md', staged: false }
|
|
})
|
|
await dispatch({
|
|
id: 'req_browser_tab_create',
|
|
method: 'browser.tabCreate',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', url: 'about:blank' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_browser_viewport',
|
|
method: 'browser.viewport',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', page: 'page-1', width: 390, height: 844 }
|
|
})
|
|
await dispatch({
|
|
id: 'req_browser_certificate_proceed',
|
|
method: 'browser.certificate.proceed',
|
|
deviceToken: mobile.token,
|
|
params: {
|
|
worktree: 'id:wt-1',
|
|
page: 'page-1',
|
|
challengeId: 'challenge-1'
|
|
}
|
|
})
|
|
await dispatch({
|
|
id: 'req_browser_dialog_accept',
|
|
method: 'browser.dialogAccept',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', page: 'page-1', text: 'ok' }
|
|
})
|
|
await dispatch({
|
|
id: 'req_browser_dialog_dismiss',
|
|
method: 'browser.dialogDismiss',
|
|
deviceToken: mobile.token,
|
|
params: { worktree: 'id:wt-1', page: 'page-1' }
|
|
})
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({
|
|
id: 'req_forbidden',
|
|
ok: false,
|
|
error: expect.objectContaining({ code: 'forbidden' })
|
|
})
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_allowed', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_settings_get', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_settings_update', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_projects', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_issue_types', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_project_labels', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_assignees', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_update_issue', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_update_issue_type', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_update_field', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_clear_field', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_update_pr', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_add_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_update_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_project_delete_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_update_issue', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_labels', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_assignees', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_add_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_add_review_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_reply_review_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_pr_file_contents', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_rerun_checks', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_resolve_thread', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_file_viewed', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_github_request_reviewers', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_github_merge_pr', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_gitlab_add_issue_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_gitlab_add_mr_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_gitlab_merge_mr', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_linear_search', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_linear_select_workspace', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_linear_team_labels', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_linear_team_members', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_linear_add_comment', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_status', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_push', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_upstream', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_git_rebase_from_base', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_bulk_stage', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_abort_merge', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_git_abort_rebase', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_git_bulk_unstage', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_select_claude', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_select_codex', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_consume_codex_reset', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_terminal_read', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_files_open_diff', ok: true }))
|
|
expect(replies).toContainEqual(expect.objectContaining({ id: 'req_git_diff', ok: true }))
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_browser_tab_create', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_browser_viewport', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({
|
|
id: 'req_browser_certificate_proceed',
|
|
ok: false,
|
|
error: expect.objectContaining({ code: 'forbidden' })
|
|
})
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_browser_dialog_accept', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({ id: 'req_browser_dialog_dismiss', ok: true })
|
|
)
|
|
expect(replies).toContainEqual(
|
|
expect.objectContaining({
|
|
id: 'req_remove_claude',
|
|
ok: false,
|
|
error: expect.objectContaining({ code: 'forbidden' })
|
|
})
|
|
)
|
|
expect(mocks.selectClaudeAccount).toHaveBeenCalledWith('claude-account')
|
|
expect(mocks.selectCodexAccount).toHaveBeenCalledWith(null)
|
|
expect(mocks.consumeCodexRateLimitResetCredit).toHaveBeenCalledWith(
|
|
'11111111-1111-4111-8111-111111111111',
|
|
expectedCodexResetScope
|
|
)
|
|
expect(mocks.readTerminal).toHaveBeenCalledWith('term-1', { cursor: undefined })
|
|
expect(mocks.getRuntimeGitStatus).toHaveBeenCalledWith('id:wt-1', { admissionTier: 'status' })
|
|
expect(mocks.pushRuntimeGit).toHaveBeenCalledWith('id:wt-1', true, undefined, undefined)
|
|
expect(mocks.getRuntimeGitUpstreamStatus).toHaveBeenCalledWith('id:wt-1')
|
|
expect(mocks.bulkStageRuntimeGitPaths).toHaveBeenCalledWith(
|
|
'id:wt-1',
|
|
['a.ts', 'b.ts'],
|
|
undefined
|
|
)
|
|
expect(mocks.abortRuntimeGitMerge).toHaveBeenCalledWith('id:wt-1')
|
|
expect(mocks.abortRuntimeGitRebase).toHaveBeenCalledWith('id:wt-1')
|
|
expect(mocks.bulkUnstageRuntimeGitPaths).toHaveBeenCalledWith('id:wt-1', ['c.ts'])
|
|
expect(mocks.openMobileDiff).toHaveBeenCalledWith('id:wt-1', 'docs/readme.md', true, undefined)
|
|
// A mobile WebSocket client is transport-capped; a local caller gets undefined here.
|
|
expect(mocks.getRuntimeGitDiff).toHaveBeenCalledWith(
|
|
'id:wt-1',
|
|
'docs/readme.md',
|
|
false,
|
|
undefined,
|
|
remoteRpcContentBudget('req_git_diff')
|
|
)
|
|
expect(mocks.browserTabCreate).toHaveBeenCalledWith(
|
|
{ worktree: 'id:wt-1', url: 'about:blank' },
|
|
{ pairedDeviceId: mobile.deviceId, clientKind: 'mobile' }
|
|
)
|
|
expect(mocks.browserSetViewport).toHaveBeenCalledWith({
|
|
worktree: 'id:wt-1',
|
|
page: 'page-1',
|
|
width: 390,
|
|
height: 844
|
|
})
|
|
expect(mocks.browserDialogAccept).toHaveBeenCalledWith({
|
|
worktree: 'id:wt-1',
|
|
page: 'page-1',
|
|
text: 'ok'
|
|
})
|
|
expect(mocks.browserDialogDismiss).toHaveBeenCalledWith({
|
|
worktree: 'id:wt-1',
|
|
page: 'page-1'
|
|
})
|
|
expect(mocks.listGitHubIssueTypesBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca'
|
|
})
|
|
expect(mocks.listGitHubLabelsBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca'
|
|
})
|
|
expect(mocks.listGitHubAssignableUsersBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
seedLogins: ['alex']
|
|
})
|
|
expect(mocks.updateGitHubIssueBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
updates: { title: 'New title' }
|
|
})
|
|
expect(mocks.updateGitHubIssueTypeBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
issueTypeId: 'type-1'
|
|
})
|
|
expect(mocks.updateGitHubPullRequestBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 456,
|
|
updates: { state: 'closed' }
|
|
})
|
|
expect(mocks.addGitHubIssueCommentBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
number: 123,
|
|
body: 'done'
|
|
})
|
|
expect(mocks.updateGitHubIssueCommentBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
commentId: 101,
|
|
body: 'edited'
|
|
})
|
|
expect(mocks.deleteGitHubIssueCommentBySlug).toHaveBeenCalledWith({
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
commentId: 101
|
|
})
|
|
expect(mocks.updateRepoIssue).toHaveBeenCalledWith('id:repo-1', 123, {
|
|
title: 'New title',
|
|
addLabels: ['bug']
|
|
})
|
|
expect(mocks.listRepoLabels).toHaveBeenCalledWith('id:repo-1')
|
|
expect(mocks.listRepoAssignableUsers).toHaveBeenCalledWith('id:repo-1')
|
|
expect(mocks.addRepoIssueComment).toHaveBeenCalledWith('id:repo-1', 123, 'done', null)
|
|
expect(mocks.addRepoPRReviewComment).toHaveBeenCalledWith('id:repo-1', {
|
|
prNumber: 456,
|
|
commitId: 'abc123',
|
|
path: 'src/app.ts',
|
|
line: 10,
|
|
startLine: undefined,
|
|
body: 'please fix',
|
|
prRepo: null
|
|
})
|
|
expect(mocks.addRepoPRReviewCommentReply).toHaveBeenCalledWith('id:repo-1', {
|
|
prNumber: 456,
|
|
commentId: 99,
|
|
body: 'fixed',
|
|
threadId: 'thread-1',
|
|
path: 'src/app.ts',
|
|
line: 10,
|
|
prRepo: null
|
|
})
|
|
expect(mocks.getRepoPRFileContents).toHaveBeenCalledWith('id:repo-1', {
|
|
prNumber: 456,
|
|
path: 'src/app.ts',
|
|
oldPath: undefined,
|
|
status: 'modified',
|
|
headSha: 'abc123',
|
|
baseSha: 'def456',
|
|
prRepo: null
|
|
})
|
|
expect(mocks.rerunRepoPRChecks).toHaveBeenCalledWith('id:repo-1', 456, {
|
|
headSha: 'abc123',
|
|
failedOnly: true,
|
|
prRepo: null
|
|
})
|
|
expect(mocks.resolveRepoReviewThread).toHaveBeenCalledWith('id:repo-1', 'thread-1', true, null)
|
|
expect(mocks.setRepoPRFileViewed).toHaveBeenCalledWith('id:repo-1', {
|
|
pullRequestId: 'PR_kw',
|
|
path: 'src/app.ts',
|
|
viewed: true,
|
|
prRepo: null
|
|
})
|
|
expect(mocks.requestRepoPRReviewers).toHaveBeenCalledWith('id:repo-1', 456, ['alex'], null)
|
|
expect(mocks.mergeRepoPR).toHaveBeenCalledWith('id:repo-1', 456, 'squash', null)
|
|
expect(mocks.addGitLabRepoIssueComment).toHaveBeenCalledWith(
|
|
'id:repo-1',
|
|
123,
|
|
'done',
|
|
undefined
|
|
)
|
|
expect(mocks.addGitLabRepoMRComment).toHaveBeenCalledWith(
|
|
'id:repo-1',
|
|
456,
|
|
'ship it',
|
|
undefined
|
|
)
|
|
expect(mocks.resolveGitLabRepoMRDiscussion).toHaveBeenCalledWith(
|
|
'id:repo-1',
|
|
456,
|
|
'discussion-1',
|
|
true,
|
|
undefined
|
|
)
|
|
expect(mocks.mergeGitLabRepoMR).toHaveBeenCalledWith('id:repo-1', 456, 'merge', undefined)
|
|
expect(mocks.updateGitHubProjectItemField).toHaveBeenCalledWith({
|
|
projectId: 'project-1',
|
|
itemId: 'item-1',
|
|
fieldId: 'field-1',
|
|
value: { kind: 'text', text: 'Ready' }
|
|
})
|
|
expect(mocks.clearGitHubProjectItemField).toHaveBeenCalledWith({
|
|
projectId: 'project-1',
|
|
itemId: 'item-1',
|
|
fieldId: 'field-1'
|
|
})
|
|
expect(mocks.linearSearchIssues).toHaveBeenCalledWith('auth', 10, 'workspace-1')
|
|
expect(mocks.linearSelectWorkspace).toHaveBeenCalledWith('workspace-1')
|
|
expect(mocks.linearTeamLabels).toHaveBeenCalledWith('team-1', 'workspace-1')
|
|
expect(mocks.linearTeamMembers).toHaveBeenCalledWith('team-1', 'workspace-1')
|
|
expect(mocks.linearAddIssueComment).toHaveBeenCalledWith('issue-1', 'done', 'workspace-1')
|
|
expect(mocks.removeClaudeAccount).not.toHaveBeenCalled()
|
|
})
|
|
})
|