Skip to content

[PM-38443] feat: Add FillAssistRepository and sync integration#2812

Open
andrebispo5 wants to merge 24 commits into
mainfrom
pm-38443/consume-fill-assist-data-and-cache
Open

[PM-38443] feat: Add FillAssistRepository and sync integration#2812
andrebispo5 wants to merge 24 commits into
mainfrom
pm-38443/consume-fill-assist-data-and-cache

Conversation

@andrebispo5

@andrebispo5 andrebispo5 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-38443

📔 Objective

Adds FillAssistRepository (protocol + DefaultFillAssistRepository) which orchestrates the full fill-assist sync pipeline: feature flag check, 6-hour interval guard, manifest fetch, cid/sourceUrl cache comparison, forms download, schema version validation, CSS selector parsing, and per-account storage. Wires the repository into SyncService (non-blocking) and ServiceContainer.

Also fixes APIService.fillAssistService to use a plain HTTPService(client: URLSession.shared) — the standard CertificateHTTPClient deliberately blocks 302 redirects for SSO, which broke the GitHub Releases CDN redirect chain.

@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:feature labels Jun 19, 2026
@andrebispo5 andrebispo5 marked this pull request as ready for review June 19, 2026 17:29
@andrebispo5 andrebispo5 requested review from a team and matt-livefront as code owners June 19, 2026 17:29
Copilot AI review requested due to automatic review settings June 19, 2026 17:29
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the new FillAssistRepository (protocol + DefaultFillAssistRepository) sync pipeline, its wiring into SyncService and ServiceContainer, and the APIService.fillAssistService redirect fix. The repository now ships with a 284-line test file covering the feature-flag guard, interval guard, cid/sourceUrl cache comparison, schema validation, selector pooling, and error swallowing — addressing the missing-coverage feedback from earlier review rounds. Fill-assist cache holds public CSS targeting rules in AppSettingsStore, which is appropriate (no vault data, no zero-knowledge concern).

Code Review Details

No new findings. The one substantive concern — replaceData awaits fillAssistRepository.syncFillAssistRules() (manifest fetch + forms download over an external CDN) before onFetchSyncSucceeded, which delays user-visible vault-sync completion despite the "must not block vault sync" comment — is already tracked in the existing unresolved review thread on SyncService.swift:482-484. Worth resolving there (e.g. kicking the fill-assist sync off in a detached task) before merge.

Prior round findings (missing repository tests, integrity-check re-encoding, unused Services typealias, schema-major guard) have been resolved or addressed in this revision.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new Fill-Assist sync orchestration layer (FillAssistRepository + DefaultFillAssistRepository) and wires it into the app’s dependency graph and vault sync flow so Fill-Assist rules can be fetched, validated, cached per-account, and consulted by hostname.

Changes:

  • Added FillAssistRepository / DefaultFillAssistRepository implementing the fill-assist sync + caching pipeline.
  • Integrated the repository into ServiceContainer/Services and injected it into DefaultSyncService.
  • Updated mocks/tests wiring to accommodate the new dependency.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
BitwardenShared/Core/Vault/Services/SyncServiceTests.swift Injects a MockFillAssistRepository into DefaultSyncService test setup.
BitwardenShared/Core/Vault/Services/SyncService.swift Calls fill-assist sync during vault sync completion path.
BitwardenShared/Core/Platform/Services/TestHelpers/ServiceContainer+Mocks.swift Adds fillAssistRepository to mock container factory.
BitwardenShared/Core/Platform/Services/Services.swift Adds HasFillAssistRepository to the Services composition.
BitwardenShared/Core/Platform/Services/ServiceContainer.swift Instantiates DefaultFillAssistRepository and injects it into DefaultSyncService and container.
BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift New repository implementing manifest fetch, schema check, integrity check, parsing, and per-account caching.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +482 to +484
// Sync fill-assist rules alongside vault data. Failures must not block vault sync.
await fillAssistRepository.syncFillAssistRules()

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment on lines +33 to +37
typealias Services = HasConfigService
& HasEnvironmentService
& HasFillAssistAPIService
& HasStateService

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.06897% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.02%. Comparing base (e9514dc) to head (b2286cc).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...form/Settings/Settings/AutoFill/AutoFillView.swift 4.16% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2812      +/-   ##
==========================================
- Coverage   81.24%   79.02%   -2.22%     
==========================================
  Files        1028     1153     +125     
  Lines       66164    73732    +7568     
==========================================
+ Hits        53756    58268    +4512     
- Misses      12408    15464    +3056     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andrebispo5 andrebispo5 marked this pull request as draft June 23, 2026 15:12
@andrebispo5 andrebispo5 marked this pull request as ready for review June 25, 2026 17:28
Comment on lines +9 to +22
/// Fetches and caches fill-assist rules for the active account.
///
func syncFillAssistRules() async

/// Returns the cached fill-assist rules for a given hostname, or `nil` if unavailable.
///
/// - Parameter hostname: The hostname to look up.
/// - Returns: The cached `FillAssistHostRules`, or `nil`.
///
func fillAssistRules(for hostname: String) async -> FillAssistHostRules?

/// Clears all cached fill-assist data for the active account.
///
func clearFillAssistRules() async throws

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Given that this is the "Fill Assist" repository, I think we can remove the use of "FillAssist" in the function names as it's a bit repetitive IMO.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
let sourceUrl = environmentService.fillAssistRulesURL
let userId = try await stateService.getActiveAccountId()
let lastFetch = appSettingsStore.fillAssistLastFetchTimestamp(userId: userId)
if let lastFetch, Date().timeIntervalSince(lastFetch) < Constants.fillAssistUpdateInterval {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Use TimeProvider instead of Date() so you can better mock the date on the tests.
This applies to all Date() in this file.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
)
}

// Sync fill-assist rules alongside vault data. Failures must not block vault sync.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ I'd remove "Failures must not block vault sync." as the function doesn't throw any errors so I think it's a direct assumption.

}

// Sync fill-assist rules alongside vault data. Failures must not block vault sync.
await fillAssistRepository.syncFillAssistRules()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Add tests for this.

Comment on lines +227 to +283
private func makeManifest(cid: String) throws -> FillAssistManifestResponseModel {
let json = """
{
"buildId": "v1",
"gitSha": "abc",
"maps": { "forms": { "v1": {
"cid": "\(cid)",
"filename": "forms.v1.json",
"schema": "forms.v1.schema.json"
}}},
"timestamp": "2026-06-11T14:29:32.184Z"
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FillAssistManifestResponseModel.self,
from: Data(json.utf8),
)
}

private func makeFormsMapWithPathnames() throws -> FormsMapResponseModel {
let json = """
{
"schemaVersion": "1.0.0",
"hosts": {
"example.com": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user1"] } }],
"pathnames": {
"/login": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user2"] } }]
}
}
}
}
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FormsMapResponseModel.self,
from: Data(json.utf8),
)
}

private func makeFormsMap(schemaVersion: String = "1.0.0") throws -> FormsMapResponseModel {
let json = """
{
"schemaVersion": "\(schemaVersion)",
"hosts": {
"example.com": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user"] } }]
}
}
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FormsMapResponseModel.self,
from: Data(json.utf8),
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Can't we directly initialize the structs instead of using JSON decoding?

Base automatically changed from pm-38443/fill-assist-storage to main June 29, 2026 17:13
…epositoryTests

- Add @mainactor so featureFlagsBool mutations compile in Swift Testing
- Move helper methods to a private extension to satisfy type_body_length
…t fillAssistClient for tests

- Filter pooled entries with empty attribute arrays before writing host rules,
  so hosts with only unsupported selectors (e.g. shadow DOM) are correctly excluded
- Add fillAssistClient parameter to APIService.init (defaults to URLSession.shared)
  so FillAssistAPIServiceTests can inject MockHTTPClient for download calls
@andrebispo5 andrebispo5 force-pushed the pm-38443/consume-fill-assist-data-and-cache branch from 6b5d552 to 4d9842c Compare June 29, 2026 17:15
@andrebispo5 andrebispo5 requested a review from fedemkr June 29, 2026 20:21
…uct initialization

Add memberwise inits to FormsMapResponseModel and FormsMapHostEntry so
test helpers can construct fixtures without JSON string construction.
Comment thread BitwardenShared/Core/Autofill/Models/FormsMapResponseModel.swift
Comment thread BitwardenShared/Core/Autofill/Models/FormsMapResponseModel.swift
Comment on lines +294 to +352
func makeManifest(cid: String) -> FillAssistManifestResponseModel {
let entry = FillAssistManifestEntryModel(
cid: cid,
deprecated: false,
filename: "forms.v1.json",
schema: "forms.v1.schema.json",
)
return FillAssistManifestResponseModel(
buildId: "v1",
gitSha: "abc",
maps: ["forms": ["v1": entry]],
timestamp: Date(timeIntervalSinceReferenceDate: 0),
)
}

private func makeFormsMap(
schemaVersion: String = "1.0.0",
usernameSelector: String = "input#user",
hosts: [String: String]? = nil,
) -> FormsMapResponseModel {
let content = { (selector: String) in
FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single(selector)]],
actions: nil,
)
}
let resolvedHosts: [String: FormsMapHostEntry] = if let hosts {
hosts.mapValues { FormsMapHostEntry(forms: [content($0)]) }
} else {
["example.com": FormsMapHostEntry(forms: [content(usernameSelector)])]
}
return FormsMapResponseModel(hosts: resolvedHosts, schemaVersion: schemaVersion)
}

private func makeFormsMapWithPathnames() -> FormsMapResponseModel {
let loginContent = FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single("input#user1")]],
actions: nil,
)
let pathnameContent = FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single("input#user2")]],
actions: nil,
)
return FormsMapResponseModel(
hosts: [
"example.com": FormsMapHostEntry(
forms: [loginContent],
pathnames: ["/login": FormsMapPathnameEntry(forms: [pathnameContent])],
),
],
schemaVersion: "1.0.0",
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Add function docs.

@github-actions github-actions Bot added the app:authenticator Bitwarden Authenticator app context label Jul 2, 2026
@andrebispo5 andrebispo5 requested a review from fedemkr July 2, 2026 10:45
andrebispo5 and others added 12 commits July 10, 2026 10:06
…c-skip behavior

syncRules_withinUpdateInterval now also stubs cached data to match the
updated skip condition, and a new test covers a fresh timestamp with
no readable cache. Fixture builders moved to a separate file to stay
under the file length limit.
Adds setFillAssistEnabledError to MockStateService and a test verifying
AutoFillProcessor reverts the toggle and shows an alert when persisting
the preference fails.
…uests

The tests still mocked the old download(filename:)-based HTTPClient
API, but the production code was migrated to fillAssistService.send(_:)
in an earlier commit. Update the mock setup and assertions to match.
The reprompt handler only proceeded when the app extension's canAutofill
was true, blocking Fill Assist-driven autofill from the main app context.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants