import test from "node:test"; import assert from "node:assert/strict"; import { createSudoPasswordAutofill, getSingleBracketedPasteLine, isExplicitSudoPrompt, isSudoPasswordPrompt, shouldArmSudoPasswordAutofill, shouldDismissPasswordAssistOnInput, } from "./terminalSudoAutofill"; // --- isSudoPasswordPrompt: relaxed — any password/密码/口令 line ending in a // colon. Over-matching is safe now because filling requires explicit confirm. --- test("isSudoPasswordPrompt detects sudo and PAM prompts", () => { assert.equal(isSudoPasswordPrompt("[sudo] password for alice: "), true); assert.equal(isSudoPasswordPrompt("Password: "), true); assert.equal(isSudoPasswordPrompt("password for alice: "), true); assert.equal(isSudoPasswordPrompt("[sudo: [sudo] password for alice: ] Password: "), true); }); test("isSudoPasswordPrompt detects localized prompts", () => { assert.equal(isSudoPasswordPrompt("[sudo] alice 的密码:"), true); assert.equal(isSudoPasswordPrompt("密码:"), true); assert.equal(isSudoPasswordPrompt("请输入密码: "), true); }); test("isSudoPasswordPrompt matches Kylin-style prompts without trailing colon", () => { // Kylin Professional: sudo prompt has no [sudo] tag and no trailing colon (#1293) assert.equal(isSudoPasswordPrompt("密码"), true); assert.equal(isSudoPasswordPrompt("用户 的密码"), true); assert.equal(isSudoPasswordPrompt("密码 "), true); // Exact prompts from issue #1293 screenshots (sudo -s on Kylin V10) assert.equal(isSudoPasswordPrompt("输入密码"), true); assert.equal(isSudoPasswordPrompt("Input Password"), true); }); test("isExplicitSudoPrompt matches Kylin-style prompts", () => { // Kylin-style [sudo] prompt without trailing colon assert.equal(isExplicitSudoPrompt("[sudo] 密码"), true); assert.equal(isExplicitSudoPrompt("[sudo] password for alice"), true); }); test("handleOutput hints on Kylin screenshot sudo prompts when armed", () => { const { autofill, hints, writes } = make(); autofill.armForCommand("sudo -s"); autofill.handleOutput("输入密码"); assert.deepEqual(hints, [true]); assert.deepEqual(writes, []); assert.equal(autofill.isPromptPending(), true); const english = make(); english.autofill.armForCommand("sudo -s"); english.autofill.handleOutput("Input Password"); assert.deepEqual(english.hints, [true]); assert.deepEqual(english.writes, []); }); test("isSudoPasswordPrompt detects color-wrapped prompts", () => { assert.equal(isSudoPasswordPrompt("\x1b[32m[sudo] password for alice: \x1b[0m"), true); }); test("isSudoPasswordPrompt ignores ordinary output", () => { assert.equal(isSudoPasswordPrompt("try sudo if the password is required\n"), false); assert.equal(isSudoPasswordPrompt("the password was changed\n"), false); assert.equal(isSudoPasswordPrompt("sudo: command not found\n"), false); }); test("isSudoPasswordPrompt refuses concealed prompt text", () => { assert.equal(isSudoPasswordPrompt("\x1b[8m[sudo] password for alice: \x1b[0m"), false); }); // --- arm + hint (confirm-to-fill) --- const make = (password = "secret") => { const writes: string[] = []; const hints: boolean[] = []; const autofill = createSudoPasswordAutofill({ password, write: (d) => writes.push(d), onHint: (active) => { hints.push(active); return true; // hint overlay shown successfully }, }); return { autofill, writes, hints }; }; test("shows a hint (not a fill) when a sudo prompt appears", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("sudo whoami"); assert.equal( autofill.handleOutput("[sudo] password for alice: "), "[sudo] password for alice: ", ); assert.deepEqual(hints, [true]); assert.deepEqual(writes, []); assert.equal(autofill.isPromptPending(), true); }); test("confirmFill writes the password and clears the hint", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); autofill.confirmFill(); assert.deepEqual(writes, ["secret\n"]); assert.deepEqual(hints, [true, false]); assert.equal(autofill.isPromptPending(), false); }); test("cancelHint clears the hint without filling", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); autofill.cancelHint(); assert.deepEqual(writes, []); assert.deepEqual(hints, [true, false]); assert.equal(autofill.isPromptPending(), false); }); // --- paste dismisses assist so Enter is not hijacked (#2198) --- test("shouldDismissPasswordAssistOnInput detects paste and typed content", () => { assert.equal(shouldDismissPasswordAssistOnInput("remote-secret"), true); assert.equal(shouldDismissPasswordAssistOnInput("\x1b[200~remote-secret\x1b[201~"), true); assert.equal(shouldDismissPasswordAssistOnInput("x"), true); // Enter is confirmFill, not user content assert.equal(shouldDismissPasswordAssistOnInput("\r"), false); assert.equal(shouldDismissPasswordAssistOnInput("\n"), false); // Control keys are handled separately assert.equal(shouldDismissPasswordAssistOnInput("\x7f"), false); assert.equal(shouldDismissPasswordAssistOnInput("\x1b"), false); assert.equal(shouldDismissPasswordAssistOnInput(""), false); }); test("clipboard paste dismisses pending hint so confirmFill no longer fires", () => { // Nested SSH: assist offers jump-host password, user pastes the remote host // password from clipboard, then presses Enter — Enter must submit the paste, // not append the saved host password (#2198). const { autofill, writes, hints } = make("jump-host-password"); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.isPromptPending(), true); assert.equal( autofill.dismissOnUserContentInput("\x1b[200~remote-host-password\x1b[201~"), true, ); assert.equal(autofill.isPromptPending(), false); assert.deepEqual(hints, [true, false]); // Simulated Enter after paste must not inject the jump-host password autofill.confirmFill(); assert.deepEqual(writes, []); }); test("plain multi-char paste dismisses pending hint", () => { const { autofill, writes, hints } = make("jump-host-password"); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.dismissOnUserContentInput("remote-host-password"), true); assert.equal(autofill.isPromptPending(), false); assert.deepEqual(hints, [true, false]); autofill.confirmFill(); assert.deepEqual(writes, []); }); test("Enter alone does not dismiss via dismissOnUserContentInput", () => { const { autofill, hints } = make(); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.dismissOnUserContentInput("\r"), false); assert.equal(autofill.isPromptPending(), true); assert.deepEqual(hints, [true]); }); test("Esc soft-dismiss keeps arm so assist can re-open on the same Password prompt", () => { const writes: string[] = []; const pickerActives: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: (d) => writes.push(d), onPicker: (active) => { pickerActives.push(active); return true; }, }); autofill.armForCommand("su root"); autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), true); autofill.cancelHint(); assert.equal(autofill.isPickerPending(), false); assert.equal(autofill.canReshowAssist(), true); // Same static prompt: more output without a new line must not auto-reopen autofill.handleOutput(""); assert.equal(autofill.isPickerPending(), false); // Explicit re-open (Esc / arrows in the UI) assert.equal(autofill.tryReshowAssist(), true); assert.equal(autofill.isPickerPending(), true); autofill.confirmFill("host"); assert.deepEqual(writes, ["host-secret\n"]); }); test("abort hard-disarms so a later bare Password requires a fresh su arm (#2191)", () => { // Ctrl+C aborts the remote su. Soft-dismiss would leave dismissedWhileArmed // and block the next bare Password: without a leading newline. const pickerActives: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: () => {}, onPicker: (active) => { pickerActives.push(active); return true; }, }); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), true); autofill.abort(); assert.equal(autofill.isPickerPending(), false); assert.equal(autofill.canReshowAssist(), false); // Stale arm gone: bare Password without a new su must not open the picker autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), false); // Fresh arm after interrupt works again autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), true); }); test("confirmFill does nothing when no prompt is pending", () => { const { autofill, writes } = make(); autofill.confirmFill(); assert.deepEqual(writes, []); }); test("does not arm when the hint cannot be shown (overlay unavailable)", () => { // If onHint reports the hint could not render (e.g. autocomplete disabled, no // ghost overlay), we must NOT leave a pending arm — otherwise Enter would // submit the sudo password with no visible confirmation. const writes: string[] = []; const autofill = createSudoPasswordAutofill({ password: "secret", write: (d) => writes.push(d), onHint: () => false, }); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.isPromptPending(), false); autofill.confirmFill(); assert.deepEqual(writes, []); }); test("a bare Password prompt does not hint until a su command is submitted", () => { const { autofill, hints } = make(); autofill.handleOutput("Password: "); assert.deepEqual(hints, []); // sudo-armed bare Password: is too generic (mysql/ssh); su-armed is expected autofill.armForCommand("sudo whoami"); autofill.handleOutput("Password: "); assert.deepEqual(hints, []); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.deepEqual(hints, [true]); }); test("an explicit [sudo] prompt hints without a recorded sudo command", () => { // The [sudo] tag is sudo-specific, so we hint even when arming didn't fire — // manual typing's recordedCommand is flaky (#1281/#1284), and the hint only // pastes on explicit Enter, so showing it is safe. const { autofill, hints } = make(); autofill.handleOutput("[sudo] password for alice: "); assert.deepEqual(hints, [true]); assert.equal(autofill.isPromptPending(), true); }); test("no hint without a saved password", () => { const { autofill, hints } = make(""); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.deepEqual(hints, []); }); test("hint fires once across chunked prompt output", () => { const { autofill, hints } = make(); autofill.armForCommand("sudo apt update"); autofill.handleOutput("[sudo] password "); autofill.handleOutput("for alice: "); assert.deepEqual(hints, [true]); }); test("cached sudo then child Enter password does not assist", () => { // sudo auth already cached: `sudo mysql -p` goes straight to mysql's // "Enter password:" — must not offer the host SSH password. const { autofill, hints, writes } = make(); autofill.armForCommand("sudo mysql -p"); autofill.handleOutput("Enter password: "); assert.deepEqual(hints, []); assert.deepEqual(writes, []); assert.equal(autofill.isPromptPending(), false); }); test("sudo-scoped bare prompts still assist when armed", () => { // Kylin / PAM without [sudo] tag (#1293) const kylink = make(); kylink.autofill.armForCommand("sudo -s"); kylink.autofill.handleOutput("输入密码"); assert.deepEqual(kylink.hints, [true]); const scoped = make(); scoped.autofill.armForCommand("sudo whoami"); scoped.autofill.handleOutput("password for alice: "); assert.deepEqual(scoped.hints, [true]); }); test("a later non-sudo command disarms the pending hint", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.deepEqual(hints, [true]); autofill.armForCommand("mysql -p"); // non-sudo/su command clears the arm assert.deepEqual(hints, [true, false]); autofill.confirmFill(); assert.deepEqual(writes, []); }); test("clears a pending hint when output moves past the prompt", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.isPromptPending(), true); // user never pressed Enter; sudo times out and returns to the shell autofill.handleOutput("\r\nsudo: timed out reading password\r\nalice@host:~$ "); assert.equal(autofill.isPromptPending(), false); assert.deepEqual(hints, [true, false]); // hint was hidden autofill.confirmFill(); assert.deepEqual(writes, []); // a later Enter no longer sends the password }); test("keeps the hint pending when sudo re-prompts after a wrong password", () => { const { autofill, hints } = make(); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); autofill.handleOutput("\r\nSorry, try again.\r\n[sudo] password for alice: "); assert.equal(autofill.isPromptPending(), true); assert.deepEqual(hints, [true]); }); test("an expired arm shows no hint for a bare prompt", () => { const writes: string[] = []; const hints: boolean[] = []; let now = 1_000; const autofill = createSudoPasswordAutofill({ password: "secret", now: () => now, write: (d) => writes.push(d), onHint: (a) => { hints.push(a); return true; }, }); autofill.armForCommand("su -"); now += 31_000; autofill.handleOutput("Password: "); assert.deepEqual(hints, []); }); test("handleOutput passes data through unchanged", () => { const { autofill } = make(); autofill.armForCommand("sudo whoami"); assert.equal( autofill.handleOutput("Reading package lists...\r\n"), "Reading package lists...\r\n", ); }); test("getSingleBracketedPasteLine extracts single-line bracketed paste content", () => { assert.equal(getSingleBracketedPasteLine("\x1b[200~sudo whoami\x1b[201~"), "sudo whoami"); assert.equal(getSingleBracketedPasteLine("\x1b[200~sudo whoami\rpwd\x1b[201~"), null); }); test("shouldArmSudoPasswordAutofill arms direct sudo and su commands", () => { assert.equal(shouldArmSudoPasswordAutofill("sudo whoami"), true); assert.equal(shouldArmSudoPasswordAutofill("command sudo whoami"), true); assert.equal(shouldArmSudoPasswordAutofill("builtin sudo whoami"), true); assert.equal(shouldArmSudoPasswordAutofill("su"), true); assert.equal(shouldArmSudoPasswordAutofill("su -"), true); assert.equal(shouldArmSudoPasswordAutofill("su root"), true); assert.equal(shouldArmSudoPasswordAutofill("su - root"), true); assert.equal(shouldArmSudoPasswordAutofill("su -l alice"), true); assert.equal(shouldArmSudoPasswordAutofill("command su -"), true); assert.equal(shouldArmSudoPasswordAutofill("builtin su"), true); assert.equal(shouldArmSudoPasswordAutofill("echo '[sudo] password for alice:'"), false); assert.equal(shouldArmSudoPasswordAutofill("cat sudo.log"), false); // Word-boundary: do not arm unrelated commands that only start with "su" assert.equal(shouldArmSudoPasswordAutofill("sum file"), false); assert.equal(shouldArmSudoPasswordAutofill("suspend"), false); assert.equal(shouldArmSudoPasswordAutofill("suricata -T"), false); assert.equal(shouldArmSudoPasswordAutofill("echo su"), false); }); test("shows a hint for su Password prompt when armed", () => { // su asks for the target account password with a bare "Password:" line // (no [sudo] tag), so arming is required (#2156). const { autofill, writes, hints } = make(); autofill.armForCommand("su -"); assert.equal(autofill.handleOutput("Password: "), "Password: "); assert.deepEqual(hints, [true]); assert.deepEqual(writes, []); assert.equal(autofill.isPromptPending(), true); autofill.confirmFill(); assert.deepEqual(writes, ["secret\n"]); }); test("su to a named user arms the same confirm-to-fill path", () => { const { autofill, writes, hints } = make(); autofill.armForCommand("su alice"); autofill.handleOutput("Password: "); assert.deepEqual(hints, [true]); autofill.confirmFill(); assert.deepEqual(writes, ["secret\n"]); }); test("hint mode does not fall back to an unrelated keychain identity", () => { // Without a session password, hint must stay silent even when password // identities exist — Enter would otherwise paste the wrong secret. const writes: string[] = []; const hints: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "hint", candidates: [ { id: "identity:root", label: "Root", username: "root", password: "root-secret" }, ], write: (d) => writes.push(d), onHint: (a) => { hints.push(a); return true; }, }); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.deepEqual(hints, []); assert.equal(autofill.isPromptPending(), false); autofill.confirmFill(); assert.deepEqual(writes, []); }); test("mode off never hints even for explicit sudo prompts", () => { const writes: string[] = []; const hints: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "off", password: "secret", write: (d) => writes.push(d), onHint: (a) => { hints.push(a); return true; }, }); autofill.handleOutput("[sudo] password for alice: "); assert.deepEqual(hints, []); assert.equal(autofill.isPromptPending(), false); }); test("isPickerPending is false during hint mode", () => { const { autofill } = make(); autofill.armForCommand("sudo whoami"); autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.isPromptPending(), true); assert.equal(autofill.isPickerPending(), false); assert.equal(autofill.moveSelection(1), false); }); test("picker mode opens the credential list and fills the selected secret", () => { const writes: string[] = []; const pickerStates: Array<{ items: { id: string }[]; selectedIndex: number } | null> = []; const hints: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", password: "host-secret", candidates: [ { id: "host", label: "Host", username: "alice", password: "host-secret" }, { id: "identity:root", label: "Root", username: "root", password: "root-secret" }, ], write: (d) => writes.push(d), onHint: (a) => { hints.push(a); return true; }, onPicker: (active, state) => { pickerStates.push(active ? { items: state!.items, selectedIndex: state!.selectedIndex } : null); return true; }, }); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.equal(autofill.isPromptPending(), true); assert.equal(pickerStates.length, 1); assert.equal(pickerStates[0]?.items.length, 2); assert.equal(pickerStates[0]?.selectedIndex, 0); assert.equal(autofill.isPickerPending(), true); assert.equal(autofill.moveSelection(1), true); assert.equal(pickerStates.at(-1)?.selectedIndex, 1); autofill.confirmFill(); assert.deepEqual(writes, ["root-secret\n"]); assert.equal(pickerStates.at(-1), null); // sudo never opens the multi-identity picker — host-password hint only const sudoPicker = createSudoPasswordAutofill({ mode: "picker", password: "host-secret", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: () => {}, onHint: (a) => { hints.push(a); return true; }, onPicker: (active, state) => { pickerStates.push(active ? { items: state!.items, selectedIndex: state!.selectedIndex } : null); return true; }, }); sudoPicker.armForCommand("sudo whoami"); sudoPicker.handleOutput("[sudo] password for alice: "); assert.equal(sudoPicker.isPickerPending(), false); assert.equal(sudoPicker.isPromptPending(), true); }); test("picker confirmFill can target a specific candidate id", () => { const writes: string[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: (d) => writes.push(d), onPicker: () => true, }); autofill.armForCommand("su root"); autofill.handleOutput("Password: "); autofill.confirmFill("identity:root"); assert.deepEqual(writes, ["root-secret\n"]); }); test("picker reopens after a wrong password when still armed", () => { const writes: string[] = []; const pickerActives: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "wrong" }, { id: "identity:root", label: "Root", password: "right" }, ], write: (d) => writes.push(d), onPicker: (active) => { pickerActives.push(active); return true; }, }); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), true); autofill.confirmFill("host"); assert.deepEqual(writes, ["wrong\n"]); assert.equal(autofill.isPickerPending(), false); // Remote rejects and re-prompts — picker should open again for another pick autofill.handleOutput("\r\nSorry, try again.\r\nPassword: "); assert.equal(autofill.isPickerPending(), true); autofill.confirmFill("identity:root"); assert.deepEqual(writes, ["wrong\n", "right\n"]); }); test("does not re-assist a child password prompt after successful fill", () => { // `sudo mysql -p`: after the sudo password is accepted, mysql's own // "Enter password:" must not reopen assist with the host secret. const { autofill, hints, writes } = make(); autofill.armForCommand("sudo mysql -p"); autofill.handleOutput("[sudo] password for alice: "); assert.deepEqual(hints, [true]); autofill.confirmFill(); assert.deepEqual(writes, ["secret\n"]); autofill.handleOutput("\r\nEnter password: "); assert.equal(autofill.isPromptPending(), false); assert.deepEqual(hints, [true, false]); // only the original sudo hint }); test("picker does not open for Enter password when sudo is already cached", () => { const writes: string[] = []; const pickerActives: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: (d) => writes.push(d), onPicker: (active) => { pickerActives.push(active); return true; }, }); autofill.armForCommand("sudo mysql -p"); autofill.handleOutput("Enter password: "); assert.equal(autofill.isPickerPending(), false); assert.deepEqual(pickerActives, []); assert.deepEqual(writes, []); }); test("picker does not open for database-style Password for user prompts after sudo", () => { const hints: boolean[] = []; const pickerActives: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", password: "host-secret", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: () => {}, onHint: (a) => { hints.push(a); return true; }, onPicker: (active) => { pickerActives.push(active); return true; }, }); autofill.armForCommand("sudo -u postgres psql -h db"); autofill.handleOutput("Password for user postgres: "); assert.equal(autofill.isPickerPending(), false); assert.deepEqual(pickerActives, []); assert.deepEqual(hints, []); }); test("picker opens for su bare Password after arm", () => { const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:root", label: "Root", password: "root-secret" }, ], write: () => {}, onPicker: () => true, }); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.equal(autofill.isPickerPending(), true); }); test("passwordless su -c ssh does not open picker for remote password", () => { const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:other", label: "Other", password: "other-secret" }, ], write: () => {}, onPicker: () => true, }); autofill.armForCommand("su bob -c 'ssh other-host'"); autofill.handleOutput("bob@other-host's password: "); assert.equal(autofill.isPickerPending(), false); assert.equal(autofill.isPromptPending(), false); }); test("picker mode requires arm before offering the full keychain list", () => { // Unarmed explicit [sudo] must not surface every identity — a remote can // forge that line. Host-password hint remains allowed (#2156 security). const writes: string[] = []; const pickerStates: Array = []; const hints: boolean[] = []; const autofill = createSudoPasswordAutofill({ mode: "picker", password: "host-secret", candidates: [ { id: "host", label: "Host", password: "host-secret" }, { id: "identity:other", label: "Other", password: "other-secret" }, ], write: (d) => writes.push(d), onHint: (a) => { hints.push(a); return true; }, onPicker: (_active, state) => { pickerStates.push(state); return true; }, }); // No armForCommand — forged remote prompt only autofill.handleOutput("[sudo] password for alice: "); assert.equal(autofill.isPickerPending(), false); assert.deepEqual(pickerStates, []); assert.deepEqual(hints, [true]); autofill.confirmFill(); assert.deepEqual(writes, ["host-secret\n"]); }); test("picker mode does not expose passwords in onPicker payload", () => { let seen: unknown = null; const autofill = createSudoPasswordAutofill({ mode: "picker", candidates: [{ id: "host", label: "Host", password: "top-secret" }], write: () => {}, onPicker: (_active, state) => { seen = state; return true; }, }); autofill.armForCommand("su -"); autofill.handleOutput("Password: "); assert.ok(seen && typeof seen === "object"); const json = JSON.stringify(seen); assert.equal(json.includes("top-secret"), false); });