How can I allow sticky posts but cap the query to 1 post?

Here’s a way to force the exact posts_per_page value in WP_Query, regardless of sticky posts or custom post injects:

$args = [
    'posts_per_page'        => 1,
    '_exact_posts_per_page' => true   // <-- our custom input argument
];

by using our custom _exact_posts_per_page input argument.

I’m sure this has been implemented many times before, but I didn’t find it at the moment, so let us try to implement it with this demo plugin:

<?php
/**
 *  Plugin Name:   Exact Posts Per Pages 
 *  Description:   Activated through the '_exact_posts_per_page' bool argument of WP_Query 
 *  Plugin URI:    http://wordpress.stackexchange.com/a/257523/26350 
 */

add_filter( 'the_posts', function( $posts, \WP_Query $q )
{
    if( 
        wp_validate_boolean( $q->get( '_exact_posts_per_page' ) )
        && ! empty( $posts ) 
        && is_int( $q->get( 'posts_per_page' ) ) 
    )
        $posts = array_slice( $posts, 0, absint( $q->get( 'posts_per_page' ) ) );

    return $posts;

}, 999, 2 ); // <-- some late priority here!

Here we use the the_posts filter to slice the array, no matter if it contains sticky posts or not.

The the_posts filter is not applied if the suppress_filters input argument of WP_Query is true.

Note that query_posts() is not recommended in plugins or themes. Check out the many warnings in the Code reference here.

Hope you can test it further and adjust to your needs!