Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/context-match.test.js
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

330 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Regression tests for the context-aware suggestion matcher in BitwardenModel.js.
//
// The matcher is heuristic and works from window titles alone, so it is easy to
// regress in both directions: too strict and the right login stops appearing,
// too loose and every .com item is suggested on every site. Run with:
//
// node tests/context-match.test.js
const fs = require("fs")
const path = require("path")
const src = fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
const Model = {}
new Function("exports", src.replace(/^\.pragma library\s*$/m, "") + `
exports.findContextualMatches = findContextualMatches
exports.cleanWindowContext = cleanWindowContext
exports.parseHost = parseHost
exports.parseAssociations = parseAssociations
exports.serializeAssociations = serializeAssociations
exports.recordAssociation = recordAssociation
exports.forgetAssociation = forgetAssociation
exports.isAssociated = isAssociated
exports.emptyAssociations = emptyAssociations
exports.MAX_TITLE_CHARS = MAX_TITLE_CHARS
exports.MAX_ASSOC_BYTES = MAX_ASSOC_BYTES
`)(Model)
const items = [
{ id: "1", name: "GitHub", uris: ["https://github.com"] },
{ id: "2", name: "Amazon", uris: ["https://www.amazon.com"] },
{ id: "3", name: "Home Assistant",uris: ["https://homeassistant.local:8123"] },
{ id: "4", name: "Google", uris: ["https://accounts.google.com"] },
{ id: "5", name: "Reddit", uris: ["https://reddit.com"] },
{ id: "6", name: "Proton Mail", uris: ["https://account.proton.me"] },
{ id: "7", name: "Cloudflare", uris: ["https://dash.cloudflare.com"] },
{ id: "8", name: "My Bank", uris: ["https://www.chase.com"] },
{ id: "9", name: "Netflix", uris: ["https://www.netflix.com"] },
{ id: "10", name: "Jellyfin", uris: ["http://192.168.1.50:8096"] },
{ id: "11", name: "GitHub (work)", uris: ["https://github.com"] },
{ id: "12", name: "BBC iPlayer", uris: ["https://www.bbc.co.uk"] },
{ id: "13", name: "Discord", uris: ["https://discord.com"] },
{ id: "14", name: "Slack", uris: ["https://acme.slack.com"] },
{ id: "15", name: "Spotify", uris: ["https://open.spotify.com"] },
{ id: "16", name: "Nextcloud", uris: ["https://cloud.example.org"] },
{ id: "17", name: "Router Admin", uris: ["http://192.168.1.1"] },
{ id: "18", name: "AWS Console", uris: ["https://console.aws.amazon.com"] },
]
// [window class, window title, expected suggestion names]
const cases = [
// Site name lives in the title but the domain does not -- the common case.
["chromium", "Settings – Home Assistant - Chromium", ["Home Assistant"]],
["chromium", "Reddit - Dive into anything - Chromium", ["Reddit"]],
["chromium", "Netflix - Chromium", ["Netflix"]],
["chromium", "Proton Mail - Chromium", ["Proton Mail"]],
["chromium", "Cloudflare Dashboard - Chromium", ["Cloudflare"]],
["chromium", "Chase Online - Credit Cards, Mortgages, Auto - Chromium", ["My Bank"]],
["chromium", "Jellyfin - Chromium", ["Jellyfin"]],
["chromium", "Files - Nextcloud - Chromium", ["Nextcloud"]],
["chromium", "Acme Corp Slack - Chromium", ["Slack"]],
["chromium", "Spotify – Web Player - Chromium", ["Spotify"]],
["chromium", "AWS Management Console - Chromium", ["AWS Console"]],
["firefox", "BBC iPlayer - Home — Mozilla Firefox", ["BBC iPlayer"]],
["chromium", "Discord | #general | My Server - Chromium", ["Discord"]],
// Sign-in prefixes, unread counters and browser branding are noise.
["firefox", "Sign in to GitHub · GitHub — Mozilla Firefox", ["GitHub", "GitHub (work)"]],
["chromium", "(3) Inbox (1,204) - me@gmail.com - Gmail - Chromium", ["Google"]],
// Brand alias: the title never says "google".
["chromium", "Gmail - Chromium", ["Google"]],
["chromium", "Untitled document - Google Docs - Chromium", ["Google"]],
// A domain in the title must not drag in every item sharing its TLD.
["chromium", "Amazon.com. Spend less. Smile more. - Chromium", ["Amazon", "AWS Console"]],
// Nothing identifiable: suggest nothing rather than guess.
["chromium", "New Tab - Chromium", []],
["chromium", "Sign in - Chromium", []],
["chromium", "How to fix config.json v1.2 errors - Stack Overflow - Chromium", []],
// Terminals: local shells describe a machine, not a credential.
["foot", "workstation: notes", []],
// ...and a remote host must not match a different domain sharing a label.
["foot", "ssh user@git.example.com", []],
// Native desktop apps match on window class.
["discord", "Discord | #general", ["Discord"]],
["spotify", "Spotify Premium", ["Spotify"]],
]
// A large vault of .com sites: a single site must not suggest all of them.
const floodItems = ["github","amazon","google","reddit","proton","cloudflare","chase",
"netflix","discord","slack","spotify","dropbox","twitch","ebay","paypal","stripe",
"linode","digitalocean","namecheap","fastmail","zoom","notion","figma","linear",
"vercel","heroku","atlassian","gitlab","bitbucket","sentry","datadog","okta",
"auth0","twilio","sendgrid","mailgun","shopify","squarespace","wordpress","medium"]
.map((b, i) => ({ id: String(i), name: b, uris: ["https://www." + b + ".com"] }))
let pass = 0
const failures = []
function check(label, ok, detail) {
if (ok) { pass++ } else { failures.push(label + "\n " + detail) }
}
const sshLike = { id: "ssh-public", name: "GitHub deploy key", typeCode: 5,
uris: ["https://github.com"], publicKey: "ssh-ed25519 AAAA" }
check("context suggestions exclude SSH public records",
Model.findContextualMatches(items.concat([sshLike]),
{ class: "chromium", title: "GitHub - Chromium", mapped: true }).matches.every(m => m.id !== "ssh-public"),
"SSH key leaked into contextual suggestions")
for (const [cls, title, expected] of cases) {
const got = Model.findContextualMatches(items, { class: cls, title, mapped: true })
.matches.map(m => m.name).sort()
check(`[${cls}] ${title}`,
JSON.stringify(got) === JSON.stringify(expected.slice().sort()),
`expected [${expected}] but got [${got}]`)
}
for (const [title, max] of [["Amazon.com. Spend less. Smile more. - Chromium", 1],
["Some Random Blog Post About Nothing - Chromium", 0]]) {
const got = Model.findContextualMatches(floodItems, { class: "chromium", title, mapped: true }).matches
check(`flood: ${title}`, got.length <= max,
`expected at most ${max} suggestion(s) from a 40-item vault, got ${got.length}: [${got.map(g => g.name)}]`)
}
// Hostname parsing feeds every domain comparison.
for (const [host, root, base] of [
["github.com", "github", "github.com"],
["dash.cloudflare.com", "cloudflare", "cloudflare.com"],
["www.bbc.co.uk", "bbc", "bbc.co.uk"],
["homeassistant.local", "homeassistant", "homeassistant.local"],
["console.aws.amazon.com", "amazon", "amazon.com"],
]) {
const p = Model.parseHost(host)
check(`parseHost(${host})`, p && p.rootName === root && p.baseDomain === base,
`expected root=${root} base=${base}, got root=${p && p.rootName} base=${p && p.baseDomain}`)
}
// The most recently focused non-shell client wins in a `hyprctl clients` list.
const clients = [
{ class: "quickshell", title: "shell", focusHistoryID: 0, mapped: true },
{ class: "chromium", title: "Netflix - Chromium", focusHistoryID: 1, mapped: true },
{ class: "foot", title: "workstation: notes", focusHistoryID: 2, mapped: true },
]
check("clients list picks most recent non-shell window",
Model.findContextualMatches(items, clients).matches.map(m => m.name).join() === "Netflix",
`got [${Model.findContextualMatches(items, clients).matches.map(m => m.name)}]`)
// ---------------------------------------------------------------------------
// Learned associations
// ---------------------------------------------------------------------------
// The case that no title heuristic can solve: an authentik portal on
// auth.example.xyz titled "Home - authentik". The title and the stored URL
// share no word at all.
// Named so that neither the name nor the URL shares a word with the page title.
const authentikItem = { id: "auth-1", name: "Personal SSO", uris: ["https://auth.example.xyz"] }
const withAuthentik = items.concat([authentikItem])
const authentikWindow = { class: "chromium", title: "Home - authentik - Chromium", mapped: true }
let assoc = Model.emptyAssociations()
check("authentik: unmatched before learning",
Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.length === 0,
`got [${Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.name)}]`)
// Pick the credential once while that window is active.
const ctx = Model.cleanWindowContext(authentikWindow)
assoc = Model.recordAssociation(assoc, ctx, authentikItem.id, "2026-08-20T00:00:00Z")
check("authentik: suggested after one pick",
Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.id).join() === "auth-1",
`got [${Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.name)}]`)
// Learning generalises across pages of the same site, which share the word.
check("authentik: generalises to another page of the same site",
Model.findContextualMatches(withAuthentik,
{ class: "chromium", title: "Applications - authentik - Chromium", mapped: true }, assoc)
.matches.map(m => m.id).join() === "auth-1",
"expected the learned item on a sibling page")
check("authentik: does not leak to unrelated sites",
Model.findContextualMatches(withAuthentik,
{ class: "chromium", title: "Netflix - Chromium", mapped: true }, assoc)
.matches.map(m => m.name).join() === "Netflix",
"a learned key must not fire on an unrelated title")
check("isAssociated reports the learned pair", Model.isAssociated(assoc, ctx, "auth-1"), "expected true")
// Last pick wins, so a key learned from the wrong page corrects itself.
const retargeted = Model.recordAssociation(assoc, ctx, "9", "2026-08-21T00:00:00Z")
check("re-picking retargets the key",
Model.findContextualMatches(withAuthentik, authentikWindow, retargeted).matches.map(m => m.id).join() === "9",
"expected the newly picked item to win")
// Explicit unlearn.
const forgotten = Model.forgetAssociation(assoc, ctx, "auth-1")
check("forgetting removes the suggestion",
Model.findContextualMatches(withAuthentik, authentikWindow, forgotten).matches.length === 0,
"expected no suggestions after forgetting")
// A learned item outranks a heuristic match on the same window.
let netflixAssoc = Model.recordAssociation(Model.emptyAssociations(),
Model.cleanWindowContext({ class: "chromium", title: "Netflix - Chromium", mapped: true }), "11")
check("learned item is ranked ahead of a heuristic match",
Model.findContextualMatches(items, { class: "chromium", title: "Netflix - Chromium", mapped: true }, netflixAssoc)
.matches[0].id === "11",
"expected the learned item first")
// Round-tripping through the on-disk format must preserve behaviour.
const roundTripped = Model.parseAssociations(Model.serializeAssociations(assoc))
check("associations survive a save/load round trip",
Model.findContextualMatches(withAuthentik, authentikWindow, roundTripped).matches.map(m => m.id).join() === "auth-1",
"expected the learned item after reload")
check("corrupt association file degrades to empty",
Model.parseAssociations("{{not json").keys && Object.keys(Model.parseAssociations("{{not json").keys).length === 0,
"expected an empty store")
const hostileAssociations = Model.parseAssociations(
'{"version":1,"keys":{"__proto__":{"polluted":true},"arbitrary":{"itemId":"x"},'
+ '"word:valid":{"itemId":"auth-1","weight":1,"count":2,"updated":"2026-08-24T00:00:00Z"}}}')
const copiedHostile = Model.recordAssociation(hostileAssociations, ctx, "auth-1", "2026-12-31T00:00:00Z")
check("association parsing drops keys outside the domain/app/word schema",
!Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "__proto__")
&& !Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "arbitrary")
&& Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "word:valid"),
Object.keys(hostileAssociations.keys).join(","))
check("hostile association keys cannot become the prototype of a copied store",
copiedHostile.keys.polluted === undefined, JSON.stringify(copiedHostile.keys))
check("association entries without a bounded string item id are dropped",
Object.keys(Model.parseAssociations(
'{"version":1,"keys":{"word:bad":{"itemId":{}},"word:good":{"itemId":"x"}}}').keys).join()
=== "word:good",
"invalid item id survived")
check("unknown association schema versions fail closed",
Object.keys(Model.parseAssociations(
'{"version":999,"keys":{"word:old":{"itemId":"x"}}}').keys).length === 0,
"unknown schema was accepted")
// Suggestions must still work with no association store at all.
check("undefined associations are safe",
Model.findContextualMatches(items, { class: "chromium", title: "Netflix - Chromium", mapped: true })
.matches.map(m => m.name).join() === "Netflix",
"expected heuristics to work without a store")
// ---------------------------------------------------------------------------
// A window title is written by the page, not by the user
// ---------------------------------------------------------------------------
//
// Everything below is about one input: document.title, chosen by whatever the
// browser is pointed at, arriving here through hyprctl. It is read on every
// panel open, on the GUI thread, with the whole vault to compare it against.
// The title reaches the matcher clipped, so the per-item work the matcher does
// over it cannot be scaled up by the page.
const longTitle = "verylongword".repeat(6000)
const clippedCtx = Model.cleanWindowContext({ class: "chromium", title: longTitle, mapped: true })
check("a page-chosen title is clipped before matching",
clippedCtx.matchText.length <= Model.MAX_TITLE_CHARS
&& clippedCtx.rawTitle.length <= Model.MAX_TITLE_CHARS,
`matchText=${clippedCtx.matchText.length} rawTitle=${clippedCtx.rawTitle.length}`)
// The host scanner used to be a single unanchored regex, and a long run of
// letters with no dot in it drove it into quadratic backtracking: ~2.5s of
// frozen shell for a 63 kB title, growing with the square of the length.
// hyprctl hands over as much as a megabyte, so this is the honest size.
const hostileTitle = "a".repeat(100000) + " - Chromium"
const hostileWindow = { class: "chromium", title: hostileTitle, mapped: true }
const bigVault = []
for (let i = 0; i < 2000; i++) {
bigVault.push({ id: `big-${i}`, name: `Account ${i}`, uris: [`https://site${i}example.com`] })
}
const started = Date.now()
Model.findContextualMatches(bigVault, hostileWindow, Model.emptyAssociations())
const elapsed = Date.now() - started
check("a hostile window title does not stall the matcher", elapsed < 2000,
`matching a 100 kB title against 2000 items took ${elapsed}ms`)
// Splitting replaced the regex, so prove it still reads the same hosts out.
for (const [text, expected] of [
["Files - Nextcloud - cloud.example.org", "example.org"],
["https://sub.example.co.uk/path", "example.co.uk"],
["see .example.com now", "example.com"],
["a..b.example.com", "example.com"],
["config.json is not a host", null],
["version 1.2.3.4 released", null],
["nothing here at all", null],
]) {
const got = Model.cleanWindowContext({ class: "chromium", title: text, mapped: true })
const base = got && got.detectedDomain ? got.detectedDomain.baseDomain : null
check(`host detection in ${JSON.stringify(text)}`, base === expected,
`expected ${expected}, got ${base}`)
}
// Every word of a title becomes a stored key, and the store is read back
// through a byte cap that turns an oversized file into no file at all. The
// write side has to stay under that cap on its own.
let grown = Model.emptyAssociations()
for (let p = 0; p < 300; p++) {
const words = []
for (let w = 0; w < 80; w++) words.push(`tok${p}x${w}zz`)
grown = Model.recordAssociation(grown,
Model.cleanWindowContext({ class: "chromium", title: words.join(" "), mapped: true }),
"auth-1", `2026-08-${String((p % 28) + 1).padStart(2, "0")}T00:00:00Z`)
}
const grownBytes = Model.serializeAssociations(grown).length
check("the association store stays inside the cap it is read back through",
grownBytes < Model.MAX_ASSOC_BYTES,
`store grew to ${grownBytes} bytes against a ${Model.MAX_ASSOC_BYTES} byte read cap`)
// Trimming must not cost the most recent lesson.
const recentCtx = Model.cleanWindowContext(authentikWindow)
const stillLearned = Model.recordAssociation(grown, recentCtx, "auth-1", "2026-12-31T00:00:00Z")
check("the newest lesson survives trimming",
Model.findContextualMatches(withAuthentik, authentikWindow, stillLearned).matches.map(m => m.id).join() === "auth-1",
"expected the just-learned item to still be suggested")
console.log(`${pass} passed, ${failures.length} failed`)
if (failures.length) {
console.error("\nFAILURES:\n " + failures.join("\n "))
process.exit(1)
}