Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions src/main/ipc/startup-manager.ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ vi.mock('fs', () => ({

// ── Mock electron ───────────────────────────────────────────────────
const mockHandlers = new Map<string, Function>()
const mockReadShortcutLink = vi.fn()
vi.mock('electron', () => ({
app: {
isPackaged: false,
Expand All @@ -74,6 +75,9 @@ vi.mock('electron', () => ({
mockHandlers.set(channel, handler)
},
},
shell: {
readShortcutLink: (...args: unknown[]) => mockReadShortcutLink(...args),
},
}))

// ── Mock platform ───────────────────────────────────────────────────
Expand Down Expand Up @@ -170,6 +174,7 @@ beforeEach(() => {
mockExistsSync.mockReturnValue(false)
mockReadFileSync.mockReturnValue('[]')
mockReaddirSync.mockReturnValue([])
mockReadShortcutLink.mockReturnValue(undefined)
// Default: all execFile calls fail (registry keys don't exist)
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: any, cb: Function) => {
cb(new Error('not found'), '', '')
Expand Down Expand Up @@ -611,6 +616,300 @@ describe('listStartupItems', () => {
})
})

// =====================================================================
// Stale entries — programs that are no longer installed (issue #245)
// =====================================================================

describe('stale startup entries', () => {
const DISABLED_FILE = 'disabled-startups.json'
const GHOST_CMD = '"C:\\Users\\User\\AppData\\Local\\DuckDuckGo\\DuckDuckGo.exe"'

/** existsSync mock where only the listed paths (plus the C: volume) exist */
function onlyExisting(...paths: string[]) {
mockExistsSync.mockImplementation((p: string) =>
p === 'C:\\' || paths.some((x) => p === x || (p as string).endsWith(x))
)
}

function lastWrittenDisabledFile(): any[] {
const calls = mockWriteFileSync.mock.calls
return JSON.parse(calls[calls.length - 1][1] as string)
}

const DAY_MS = 24 * 60 * 60 * 1000
const longGone = () => Date.now() - DAY_MS - 60_000

/** reg mock where the StartupApproved key holds a disabled marker for `name` */
function withApprovedMarker(name: string, opts: { deleteFails?: boolean } = {}) {
const regCalls: Array<{ cmd: string; args: string[] }> = []
mockExecFile.mockImplementation((cmd: string, args: string[], _opts: any, cb: Function) => {
regCalls.push({ cmd, args })
if (args[0] === 'query' && args[1].includes('StartupApproved')) {
return cb(null, ` ${name} REG_BINARY 030000000000000000000000\n`, '')
}
if (args[0] === 'delete') {
return opts.deleteFails ? cb(new Error('Access is denied'), '', '') : cb(null, '', '')
}
cb(new Error('not found'), '', '')
})
return regCalls
}

describe('disabled-file pruning', () => {
it('flags — but keeps — an entry the first time its program is missing', async () => {
// A single missing-file observation could just be an updater mid-replace
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu' },
]))
withApprovedMarker('DuckDuckGo')

const items = await listStartupItems()
const ghost = items.find((i) => i.name === 'DuckDuckGo')
expect(ghost).toBeDefined()
expect(ghost!.stale).toBe(true)
// The record is stamped so the grace period can start running
expect(lastWrittenDisabledFile()[0].missingSince).toBeTypeOf('number')
})

it('drops an entry whose program has stayed uninstalled', async () => {
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu', missingSince: longGone() },
]))
withApprovedMarker('DuckDuckGo')

const items = await listStartupItems()
expect(items.find((i) => i.name === 'DuckDuckGo')).toBeUndefined()
// The stale record is removed from disk so it cannot come back
expect(lastWrittenDisabledFile()).toEqual([])
})

it('clears the StartupApproved marker for the entry it drops', async () => {
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu', missingSince: longGone() },
]))
const regCalls = withApprovedMarker('DuckDuckGo')

const items = await listStartupItems()
expect(items.find((i) => i.name === 'DuckDuckGo')).toBeUndefined()
const markerDelete = regCalls.find(
(c) => c.args[0] === 'delete' && c.args[1].includes('StartupApproved') && c.args.includes('DuckDuckGo')
)
expect(markerDelete).toBeDefined()
expect(lastWrittenDisabledFile()).toEqual([])
})

it('restarts the clock when the program comes back', async () => {
onlyExisting(DISABLED_FILE, 'C:\\Users\\User\\AppData\\Local\\DuckDuckGo\\DuckDuckGo.exe')
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu', missingSince: longGone() },
]))

const items = await listStartupItems()
const ghost = items.find((i) => i.name === 'DuckDuckGo')
expect(ghost).toBeDefined()
expect(ghost!.stale).toBe(false)
expect(lastWrittenDisabledFile()[0].missingSince).toBeUndefined()
})

it('keeps the entry when the StartupApproved marker cannot be cleared', async () => {
// Without the stored command a reinstalled program could never be
// re-enabled, so the record has to survive a failed marker cleanup.
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'Nahimic', command: '"C:\\Program Files\\Nahimic\\Nahimic.exe"', location: HKLM_RUN, source: 'registry-hklm', missingSince: longGone() },
]))
withApprovedMarker('Nahimic', { deleteFails: true })

const items = await listStartupItems()
const retained = items.find((i) => i.name === 'Nahimic')
expect(retained).toBeDefined()
// Retained because we could not clean up — still flagged so the user can
expect(retained!.stale).toBe(true)
expect(mockWriteFileSync).not.toHaveBeenCalled()
})

it('keeps the entry when the StartupApproved key cannot be read', async () => {
// A failed lookup is not evidence that no marker exists
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu', missingSince: longGone() },
]))
// Default execFile mock fails every call, including the approved query

const items = await listStartupItems()
expect(items.find((i) => i.name === 'DuckDuckGo')).toBeDefined()
expect(mockWriteFileSync).not.toHaveBeenCalled()
})

it('drops the entry when the key is readable and holds no marker', async () => {
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu', missingSince: longGone() },
]))
const regCalls = withApprovedMarker('SomethingElse')

const items = await listStartupItems()
expect(items.find((i) => i.name === 'DuckDuckGo')).toBeUndefined()
// Nothing to delete — no marker was there
expect(regCalls.some((c) => c.args[0] === 'delete')).toBe(false)
expect(lastWrittenDisabledFile()).toEqual([])
})

it('keeps a disabled entry while its program is still installed', async () => {
onlyExisting(DISABLED_FILE, 'C:\\Users\\User\\AppData\\Local\\DuckDuckGo\\DuckDuckGo.exe')
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'DuckDuckGo', command: GHOST_CMD, location: HKCU_RUN, source: 'registry-hkcu' },
]))

const items = await listStartupItems()
const ghost = items.find((i) => i.name === 'DuckDuckGo')
expect(ghost).toBeDefined()
expect(ghost!.enabled).toBe(false)
expect(mockWriteFileSync).not.toHaveBeenCalled()
})

it('keeps a disabled entry whose target cannot be resolved', async () => {
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'Helper', command: 'rundll32.exe C:\\App\\helper.dll,Start', location: HKCU_RUN, source: 'registry-hkcu' },
]))

const items = await listStartupItems()
expect(items.find((i) => i.name === 'Helper')).toBeDefined()
expect(mockWriteFileSync).not.toHaveBeenCalled()
})

it('keeps a disabled entry when its volume is not mounted', async () => {
// Only the JSON file exists — the D: volume is absent, so "missing" is unknown
mockExistsSync.mockImplementation((p: string) => (p as string).endsWith(DISABLED_FILE))
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'PortableApp', command: '"D:\\Portable\\app.exe"', location: HKCU_RUN, source: 'registry-hkcu' },
]))

const items = await listStartupItems()
expect(items.find((i) => i.name === 'PortableApp')).toBeDefined()
expect(mockWriteFileSync).not.toHaveBeenCalled()
})

it('keeps a disabled entry that still has a live registry value', async () => {
onlyExisting(DISABLED_FILE)
mockReadFileSync.mockReturnValue(JSON.stringify([
{ name: 'Discord', command: '"C:\\Discord\\Discord.exe"', location: HKCU_RUN, source: 'registry-hkcu' },
]))
setupExecFileHandler((cmd, args) => {
if (cmd === 'reg' && args[1] === HKCU_RUN) {
return { stdout: regOutput([{ name: 'Discord', command: '"C:\\Discord\\Discord.exe"' }]) }
}
throw new Error('not found')
})

const items = await listStartupItems()
const discord = items.find((i) => i.name === 'Discord')
expect(discord).toBeDefined()
expect(discord!.enabled).toBe(false)
expect(mockWriteFileSync).not.toHaveBeenCalled()
})
})

describe('stale flag', () => {
it('flags a registry item whose executable is gone', async () => {
onlyExisting()
setupExecFileHandler((cmd, args) => {
if (cmd === 'reg' && args[1] === HKCU_RUN) {
return { stdout: regOutput([{ name: 'Nahimic', command: '"C:\\Program Files\\Nahimic\\Nahimic.exe"' }]) }
}
throw new Error('not found')
})

const items = await listStartupItems()
expect(items.find((i) => i.name === 'Nahimic')!.stale).toBe(true)
})

it('does not flag a registry item whose executable is present', async () => {
onlyExisting('C:\\Program Files\\Nahimic\\Nahimic.exe')
setupExecFileHandler((cmd, args) => {
if (cmd === 'reg' && args[1] === HKCU_RUN) {
return { stdout: regOutput([{ name: 'Nahimic', command: '"C:\\Program Files\\Nahimic\\Nahimic.exe" -tray' }]) }
}
throw new Error('not found')
})

const items = await listStartupItems()
expect(items.find((i) => i.name === 'Nahimic')!.stale).toBe(false)
})

it('never flags entries under the Windows directory', async () => {
onlyExisting()
setupExecFileHandler((cmd, args) => {
if (cmd === 'reg' && args[1] === HKLM_RUN) {
return { stdout: regOutput([{ name: 'SecurityHealth', command: 'C:\\Windows\\system32\\SecurityHealthSystray.exe' }]) }
}
throw new Error('not found')
})

const items = await listStartupItems()
expect(items.find((i) => i.name === 'SecurityHealth')!.stale).toBe(false)
})

it('flags a scheduled task pointing at a removed executable', async () => {
onlyExisting()
setupExecFileHandler((cmd) => {
if (cmd === 'powershell') {
return { stdout: 'TASK|NahimicSvc32Run|C:\\Program Files\\Nahimic\\NahimicSvc32.exe /run|Ready\n' }
}
throw new Error('not found')
})

const items = await listStartupItems()
expect(items.find((i) => i.name === 'NahimicSvc32Run')!.stale).toBe(true)
})

it('flags a startup-folder shortcut whose target is gone', async () => {
mockExistsSync.mockImplementation((p: string) => (p as string).includes('Startup') || p === 'C:\\')
mockReaddirSync.mockReturnValue(['OldApp.lnk'])
mockReadShortcutLink.mockReturnValue({ target: 'C:\\Program Files\\OldApp\\OldApp.exe' })

const items = await listStartupItems()
expect(items.find((i) => i.name === 'OldApp.lnk')!.stale).toBe(true)
})

it('does not flag a shortcut whose target cannot be read', async () => {
mockExistsSync.mockImplementation((p: string) => (p as string).includes('Startup') || p === 'C:\\')
mockReaddirSync.mockReturnValue(['OldApp.lnk'])
mockReadShortcutLink.mockImplementation(() => { throw new Error('not a shortcut') })

const items = await listStartupItems()
expect(items.find((i) => i.name === 'OldApp.lnk')!.stale).toBe(false)
})

it('expands environment variables when resolving the target', async () => {
process.env.__KUDU_TEST_APPDIR = 'C:\\Apps'
onlyExisting('C:\\Apps\\present.exe')
setupExecFileHandler((cmd, args) => {
if (cmd === 'reg' && args[1] === HKCU_RUN) {
return {
stdout: regOutput([
{ name: 'Present', command: '%__KUDU_TEST_APPDIR%\\present.exe' },
{ name: 'Gone', command: '%__KUDU_TEST_APPDIR%\\gone.exe' },
{ name: 'UnknownVar', command: '%__KUDU_NOT_SET__%\\app.exe' },
])
}
}
throw new Error('not found')
})

const items = await listStartupItems()
expect(items.find((i) => i.name === 'Present')!.stale).toBe(false)
expect(items.find((i) => i.name === 'Gone')!.stale).toBe(true)
expect(items.find((i) => i.name === 'UnknownVar')!.stale).toBe(false)
delete process.env.__KUDU_TEST_APPDIR
})
})
})

// =====================================================================
// toggleStartupItem
// =====================================================================
Expand Down Expand Up @@ -868,6 +1167,16 @@ describe('deleteStartupItem', () => {
expect(psCall!.args.join(' ')).toContain('-Confirm:$false')
})

it('only unregisters when the task still exists (already-removed tasks succeed)', async () => {
const regCalls = collectRegCalls()

const result = await deleteStartupItem('GhostTask', 'Task Scheduler', 'task-scheduler')
expect(result).toBe(true)
const script = regCalls.find((c) => c.cmd === 'powershell')!.args.join(' ')
expect(script).toContain("Get-ScheduledTask -TaskName 'GhostTask' -ErrorAction SilentlyContinue")
expect(script).toContain('if ($task) { Unregister-ScheduledTask')
})

it('rejects unsafe task names', async () => {
const result = await deleteStartupItem("evil'; DROP TABLE", 'Task Scheduler', 'task-scheduler')
expect(result).toBe(false)
Expand Down
Loading
Loading