-
Notifications
You must be signed in to change notification settings - Fork 97
feat(edge): Autonomy Edge account, cloud projects and version control on the desktop [DOPE-388] #1056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Gustavohsdp
wants to merge
34
commits into
development
Choose a base branch
from
feat/dope388/desktop-editor-cloud-login-project-sync-and-ai-on-cloud-credits
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat(edge): Autonomy Edge account, cloud projects and version control on the desktop [DOPE-388] #1056
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
f519627
feat(edge-account): hold an Edge session in the desktop editor [DOPE-…
Gustavohsdp eda4515
feat(edge-account): sign in with a provider in a window we own [DOPE-…
Gustavohsdp d0b595d
feat(edge-account): expose the Edge account to the renderer [DOPE-388]
Gustavohsdp b44e62f
feat(edge-account): tell having an Edge account apart from requiring …
Gustavohsdp affd2f1
feat(edge-account): reach the account from the start screen [DOPE-388]
Gustavohsdp 120d31d
refactor(project): move the API envelope into the shared layer [DOPE-…
Gustavohsdp c48a0ed
feat(edge-projects): open and save Autonomy Edge projects from the de…
Gustavohsdp a475c03
feat(edge-projects): list the account's recent projects on the start …
Gustavohsdp ef665a7
fix(edge-projects): never let the cloud list take the start screen do…
Gustavohsdp 458d7dd
style(edge-projects): separate the cloud section from the local one […
Gustavohsdp 2dedc85
style(edge-projects): title the section Autonomy Edge Cloud Projects …
Gustavohsdp 5560a2e
feat(edge-projects): reserve the cloud section and invite a sign-in […
Gustavohsdp 91779a2
feat(edge-projects): make the cloud invitation a control, not a capti…
Gustavohsdp 74fb9c6
style(edge-projects): span the invitation across the row and centre i…
Gustavohsdp 6064931
style(edge-projects): drop the em dash from the invitation copy [DOPE…
Gustavohsdp 155e4ff
refactor(diff-viewer): move the graphical diff onto the shared surfac…
Gustavohsdp 46709ae
feat(version-control): bring version control to the desktop editor [D…
Gustavohsdp f9e5d90
feat(commit-history): give the desktop the commit file view, and fix …
Gustavohsdp b5a3c95
feat(edge-projects): publish a local project to Autonomy Edge from th…
Gustavohsdp da10411
fix(edge-account): pick up a sign-in performed elsewhere, and hold th…
Gustavohsdp ea34611
fix(navigation): stop a routed screen the desktop lacks from restarti…
Gustavohsdp 5a5490e
refactor(diff-viewer): put the Monaco teardown in one place [DOPE-388]
Gustavohsdp 40b4fa8
feat(branches): bring branch merge to the desktop editor [DOPE-388]
Gustavohsdp a3117a5
fix(branches): drop a remembered branch that no longer exists [DOPE-388]
Gustavohsdp d713f93
fix(edge-projects): stop the editor rewriting every file on save [DOP…
Gustavohsdp 8dcbb4a
fix(edge-projects): carry the server's canEdit through to the desktop
Gustavohsdp 74da907
fix(monaco): stop a cancelled editor task reading as a runtime error
Gustavohsdp d683409
docs(version-control): correct three comments the desktop work outdated
Gustavohsdp 3c03282
style: run prettier and the import sort over the branch
Gustavohsdp fd90c78
Merge remote-tracking branch 'origin/development' into feat/dope388/d…
Gustavohsdp 8f87d23
test(device): teach two fixtures about the purchase-watch state
Gustavohsdp fa1fc2a
test(edge-account): stub Electron so the OAuth matcher runs in CI
Gustavohsdp b4fd1ae
test(edge-projects): cover what this branch added to the coverage-gat…
Gustavohsdp bf01914
Merge remote-tracking branch 'origin/development' into feat/dope388/d…
Gustavohsdp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
336 changes: 336 additions & 0 deletions
336
src/backend/editor/edge-account/__tests__/edge-account-service.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,336 @@ | ||
| /** | ||
| * The session logic, with HTTP and disk stubbed out. | ||
| * | ||
| * What is worth protecting is not the request shapes — one line each — but four | ||
| * decisions that are easy to regress and expensive when they break: | ||
| * | ||
| * - an unverified email arrives as a 200 with a null access token. Read as a failure, | ||
| * it sends someone with the right password hunting for a wrong one. | ||
| * - a transport failure must surface as `unknown`, never as `no-session`. | ||
| * - rotation is single-use, so concurrent renewals must collapse onto ONE request. | ||
| * - a refused renewal must drop the stored token, or every launch afterwards begins | ||
| * with a request that can only fail. | ||
| */ | ||
|
|
||
| import { | ||
| __resetInMemorySessionForTests, | ||
| adoptProviderTokens, | ||
| fetchPlanCaption, | ||
| fetchUser, | ||
| signIn, | ||
| signOut, | ||
| } from '../edge-account-service' | ||
| import { edgeRequest } from '../edge-http' | ||
| import { clearRefreshToken, readRefreshToken, saveRefreshToken } from '../session-store' | ||
|
|
||
| jest.mock('../edge-http', () => ({ | ||
| edgeRequest: jest.fn(), | ||
| parseJsonBody: (body: string) => { | ||
| try { | ||
| return JSON.parse(body) | ||
| } catch { | ||
| return null | ||
| } | ||
| }, | ||
| })) | ||
|
|
||
| jest.mock('../session-store', () => ({ | ||
| saveRefreshToken: jest.fn(() => ({ persisted: true })), | ||
| readRefreshToken: jest.fn(), | ||
| clearRefreshToken: jest.fn(), | ||
| isEncryptionAvailable: jest.fn(() => true), | ||
| })) | ||
|
|
||
| const request = edgeRequest as jest.MockedFunction<typeof edgeRequest> | ||
| const readStored = readRefreshToken as jest.MockedFunction<typeof readRefreshToken> | ||
| const saveStored = saveRefreshToken as jest.MockedFunction<typeof saveRefreshToken> | ||
| const clearStored = clearRefreshToken as jest.MockedFunction<typeof clearRefreshToken> | ||
|
|
||
| const USER = { id: 'u1', name: 'Ada', email: 'ada@example.com', username: 'ada' } | ||
|
|
||
| /** A JWT whose only meaningful claim is an `exp` the given distance from now. */ | ||
| function tokenExpiringIn(ms: number): string { | ||
| const payload = Buffer.from(JSON.stringify({ exp: Math.floor((Date.now() + ms) / 1000) })).toString('base64url') | ||
|
|
||
| return `header.${payload}.signature` | ||
| } | ||
|
|
||
| const LIVE_TOKEN = tokenExpiringIn(7 * 24 * 60 * 60 * 1000) | ||
|
|
||
| function ok(data: unknown) { | ||
| return { status: 200, body: JSON.stringify({ data }) } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| __resetInMemorySessionForTests() | ||
| readStored.mockReturnValue(null) | ||
| }) | ||
|
|
||
| describe('signIn', () => { | ||
| it('maps a 401 to invalid credentials', async () => { | ||
| request.mockResolvedValueOnce({ status: 401, body: '{}' }) | ||
|
|
||
| await expect(signIn('ada@example.com', 'wrong')).resolves.toEqual({ status: 'invalid-credentials' }) | ||
| expect(saveStored).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('reads a 200 with a null access token as an unverified address', async () => { | ||
| // Edge answers exactly this for a correct password on an unverified account. | ||
| request.mockResolvedValueOnce(ok({ accessToken: null, refreshToken: null, user: USER })) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ | ||
| status: 'email-unverified', | ||
| email: 'ada@example.com', | ||
| }) | ||
| }) | ||
|
|
||
| it('adopts the pair and persists only the refresh token', async () => { | ||
| request.mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r1', user: USER })) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
|
|
||
| // The access token is deliberately never written down — it lives 7 days. | ||
| expect(saveStored).toHaveBeenCalledTimes(1) | ||
| expect(saveStored).toHaveBeenCalledWith('r1') | ||
| }) | ||
|
|
||
| it('names the user with a follow-up read when the response omits one', async () => { | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r1' })) | ||
| .mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
| }) | ||
|
|
||
| it('fails, holding no half session, when the user cannot be named', async () => { | ||
| request.mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r1' })).mockResolvedValueOnce(ok({})) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'failed' }) | ||
| expect(clearStored).toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('fails on an access token with no refresh token', async () => { | ||
| // Not `email-unverified` — that case is a NULL access token. Here there is a usable | ||
| // access token and nothing to renew it with, which is a session that dies in 7 days | ||
| // with no way back. | ||
| request.mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: null, user: USER })) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'failed' }) | ||
| }) | ||
|
|
||
| it('reports a transport failure as a failed sign-in', async () => { | ||
| request.mockRejectedValueOnce(new Error('ECONNREFUSED')) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'failed' }) | ||
| }) | ||
|
|
||
| it('maps a 500 to a failed sign-in', async () => { | ||
| request.mockResolvedValueOnce({ status: 500, body: 'upstream exploded' }) | ||
|
|
||
| await expect(signIn('ada@example.com', 'right')).resolves.toEqual({ status: 'failed' }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('adoptProviderTokens', () => { | ||
| it('adopts a harvested pair and names the user', async () => { | ||
| request.mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await expect(adoptProviderTokens({ accessToken: LIVE_TOKEN, refreshToken: 'r1' })).resolves.toEqual({ | ||
| status: 'signed-in', | ||
| user: USER, | ||
| }) | ||
| expect(saveStored).toHaveBeenCalledWith('r1') | ||
| }) | ||
|
|
||
| it('fails on an incomplete pair without touching storage', async () => { | ||
| await expect(adoptProviderTokens({ accessToken: LIVE_TOKEN })).resolves.toEqual({ status: 'failed' }) | ||
| expect(saveStored).not.toHaveBeenCalled() | ||
| }) | ||
| }) | ||
|
|
||
| describe('fetchUser', () => { | ||
| it('says no-session when there is nothing to renew with', async () => { | ||
| await expect(fetchUser()).resolves.toEqual({ status: 'no-session' }) | ||
| expect(request).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('renews from the stored token, then answers', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| .mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
|
|
||
| // Restoring a session across restarts needs no separate step: the first read | ||
| // renews from disk on its own. | ||
| expect(request).toHaveBeenNthCalledWith(1, '/auth/refresh', { method: 'POST', json: { refreshToken: 'stored-r' } }) | ||
| expect(saveStored).toHaveBeenCalledWith('r2') | ||
| }) | ||
|
|
||
| it('renews once and retries when a live-looking token is refused', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| // Refused despite a future `exp`: revoked from another device, or the account's | ||
| // tokens invalidated by a password change. | ||
| .mockResolvedValueOnce({ status: 401, body: '{}' }) | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r3' })) | ||
| .mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
| expect(request).toHaveBeenCalledTimes(4) | ||
| }) | ||
|
|
||
| it('gives up after one forced renewal that fails', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| .mockResolvedValueOnce({ status: 401, body: '{}' }) | ||
| .mockResolvedValueOnce({ status: 401, body: '{}' }) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'no-session' }) | ||
| }) | ||
|
|
||
| it('surfaces a transport failure as unknown, never as no-session', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockRejectedValueOnce(new Error('offline')) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'unknown' }) | ||
| }) | ||
|
|
||
| it('says no-session when the renewal is refused, and drops the dead token', async () => { | ||
| readStored.mockReturnValue('revoked-r') | ||
| request.mockResolvedValueOnce({ status: 401, body: '{}' }) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'no-session' }) | ||
| expect(clearStored).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('keeps the token when the renewal fails with a 5xx', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockResolvedValueOnce({ status: 503, body: '' }) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'no-session' }) | ||
| // A 5xx says nothing about whether the token is valid. | ||
| expect(clearStored).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('says no-session when the profile payload carries no user', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })).mockResolvedValueOnce(ok({})) | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'no-session' }) | ||
| }) | ||
|
|
||
| it('collapses concurrent renewals onto one request', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
|
|
||
| let release: (value: { status: number; body: string }) => void = () => undefined | ||
| const pending = new Promise<{ status: number; body: string }>((resolve) => { | ||
| release = resolve | ||
| }) | ||
|
|
||
| request.mockReturnValueOnce(pending).mockResolvedValue(ok({ user: USER })) | ||
|
|
||
| const both = Promise.all([fetchUser(), fetchUser()]) | ||
| release(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
|
|
||
| await expect(both).resolves.toEqual([ | ||
| { status: 'signed-in', user: USER }, | ||
| { status: 'signed-in', user: USER }, | ||
| ]) | ||
|
|
||
| // Refresh tokens are single-use: a second renewal would present a superseded token | ||
| // and lean on the server's replay window to recover. | ||
| expect(request.mock.calls.filter(([path]) => path === '/auth/refresh')).toHaveLength(1) | ||
| }) | ||
|
|
||
| it('renews a token that is inside the expiry margin', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: tokenExpiringIn(5_000), refreshToken: 'r1', user: USER })) | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| .mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await signIn('ada@example.com', 'right') | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
| expect(request.mock.calls.filter(([path]) => path === '/auth/refresh')).toHaveLength(1) | ||
| }) | ||
|
|
||
| it('treats an unreadable token as needing renewal rather than trusting it', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: 'not-a-jwt', refreshToken: 'r1', user: USER })) | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| .mockResolvedValueOnce(ok({ user: USER })) | ||
|
|
||
| await signIn('ada@example.com', 'right') | ||
|
|
||
| await expect(fetchUser()).resolves.toEqual({ status: 'signed-in', user: USER }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('fetchPlanCaption', () => { | ||
| it('renders the plan name the way Edge does', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| .mockResolvedValueOnce(ok({ plan: { displayName: 'Pro' } })) | ||
|
|
||
| await expect(fetchPlanCaption()).resolves.toBe('Pro Plan') | ||
| }) | ||
|
|
||
| it('returns null for an account with no plan', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request | ||
| .mockResolvedValueOnce(ok({ accessToken: LIVE_TOKEN, refreshToken: 'r2' })) | ||
| // Edge answers 404 for Community, expired or cancelled — a valid state, not an | ||
| // error. | ||
| .mockResolvedValueOnce({ status: 404, body: '{}' }) | ||
|
|
||
| await expect(fetchPlanCaption()).resolves.toBeNull() | ||
| }) | ||
|
|
||
| it('returns null rather than propagating a transport failure', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockRejectedValueOnce(new Error('offline')) | ||
|
|
||
| await expect(fetchPlanCaption()).resolves.toBeNull() | ||
| }) | ||
|
|
||
| it('returns null when there is no session at all', async () => { | ||
| await expect(fetchPlanCaption()).resolves.toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe('signOut', () => { | ||
| it('revokes server-side and clears locally', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockResolvedValueOnce({ status: 200, body: '{}' }) | ||
|
|
||
| await signOut() | ||
|
|
||
| expect(clearStored).toHaveBeenCalledTimes(1) | ||
| expect(request).toHaveBeenCalledWith('/auth/logout', { method: 'POST', json: { refreshToken: 'stored-r' } }) | ||
| }) | ||
|
|
||
| it('clears locally even when the request fails', async () => { | ||
| readStored.mockReturnValue('stored-r') | ||
| request.mockRejectedValueOnce(new Error('offline')) | ||
|
|
||
| await expect(signOut()).resolves.toBeUndefined() | ||
|
|
||
| // Someone who asked to leave must end up signed out; the server-side token expires | ||
| // on its own. | ||
| expect(clearStored).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('skips the request when there is nothing to revoke', async () => { | ||
| await signOut() | ||
|
|
||
| expect(request).not.toHaveBeenCalled() | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: Autonomy-Logic/openplc-editor
Length of output: 29817
🏁 Script executed:
Repository: Autonomy-Logic/openplc-editor
Length of output: 50385
🏁 Script executed:
Repository: Autonomy-Logic/openplc-editor
Length of output: 21372
🌐 Web query:
@types/jest 30.0.0 index.d.ts jest.fn generic default Mock<any> signature💡 Result:
In
@types/jestversion 30.0.0, the jest.fn function signature and its associated Mock interface are defined to support generic type arguments for type safety [1][2]. The jest.fn function is defined with overloaded signatures [1][3]: 1. jest.fn: Returns a Mock instance, where generic parameters default to any [1]. 2. jest.fn<T, Y extends any[], C = any>(implementation?: (this: C,...args: Y) => T): Returns a Mock<T, Y, C> [1]. In the Mock<T, Y, C> interface (where T is the return type, Y is the arguments array, and C is the context/this type), the signature defaults to using any if explicit types are not provided when calling jest.fn [1]. When you provide a generic argument, such as jest.fn, TypeScript uses that signature to enforce type checking on the implementation, arguments, and return values [4]. If you omit the implementation, passing a generic type argument is the standard way to ensure the mock function is correctly typed [4]. For example, using jest.fn allows the compiler to infer the correct argument and return types for methods like.mockImplementation [4]. Top results: [4][1][2][3]Citations:
Type the Jest doubles without assertions.
as jest.MockedFunction<...>casts withjest.mocked(...).edgeAccountSignInasjest.fn<Promise<EdgeSignInOutcome>, [string, string]>(); barejest.fn()usesanydefaults.as unknown as typeof window.bridgewith a typed test seam for the five methods used byeditorEdgeAccountPort; the cast hides missing members of the fullElectronHandlercontract.📍 Affects 2 files
src/backend/editor/edge-account/__tests__/edge-account-service.test.ts#L44-L47(this comment)src/middleware/adapters/editor/__tests__/edge-account-adapter.test.ts#L16-L22src/middleware/adapters/editor/__tests__/edge-account-adapter.test.ts#L36-L36🤖 Prompt for AI Agents
Sources: Coding guidelines, Learnings