Signals end every command, not only ssh to and ssh install (closes #48)
check / check (push) Successful in 2m35s

SIGINT, SIGTERM and SIGHUP were caught for the whole run, but only the
ssh and sftp children acted on them: the mnemonic prompt waited for
Enter, and an interrupted `age encrypt -o` went on to put the
encryption of the cut-off input in place. Now only `ssh to` and
`ssh install` catch them, from once the mnemonic is read until their
cleanup has run; everywhere else they end the tool at once. Tests cover
an interrupted `age encrypt -o` and the install working directory on a
signal.

Model: opus-5-5
This commit is contained in:
2026-10-04 04:40:57 +00:00
parent 1ddc2c747a
commit ca1faa5551
6 changed files with 163 additions and 22 deletions
+56
View File
@@ -1,10 +1,14 @@
package cli_test
import (
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
"sneak.berlin/go/keyfunc/internal/agekey"
@@ -90,6 +94,58 @@ func TestARefusedDecryptionLeavesTheOutputFileAlone(t *testing.T) {
require.Equal(t, "what was already there\n", string(kept))
}
func TestASignalStopsAnEncryptionAndPutsNoFileInPlace(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
for _, ending := range []os.Signal{
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
} {
encryptionInterrupted(t, ending.String(), ending)
}
}
// encryptionInterrupted runs "age encrypt -o" as a subprocess reading
// from a pipe that stays open, waits until the tool has begun writing
// the file beside the one it was named, and sends it the signal. The
// tool has to end on the signal alone, with a failure, and leave
// nothing at the name it was given. A tool that went on reading would
// not end until the input did, and would then put the encryption of the
// cut-off input in place.
func encryptionInterrupted(t *testing.T, name string, signal os.Signal) {
t.Helper()
directory := t.TempDir()
sealed := filepath.Join(directory, "notes.age")
//nolint:gosec // the binary is this test's own, re-run as the tool
command := exec.CommandContext(
t.Context(), os.Args[0], "age", "encrypt", "-o", sealed,
)
command.Env = append(os.Environ(), runAsTool+"=1")
producer, err := command.StdinPipe()
require.NoError(t, err)
require.NoError(t, command.Start())
_, err = io.WriteString(producer, "the start of the secret\n")
require.NoError(t, err)
// The file beside the named one is made once the mnemonic has been
// read, just before the encryption starts.
require.Eventually(t, func() bool {
entries, err := os.ReadDir(directory)
return err == nil && len(entries) > 0
}, 5*time.Second, 5*time.Millisecond)
require.NoError(t, command.Process.Signal(signal))
waitForTool(t, name, command)
require.False(t, command.ProcessState.Success(), name)
require.NoFileExists(t, sealed, name)
}
// written puts the contents in a file of that name in a directory of
// this test's own and returns the path to it.
func written(t *testing.T, name, contents string) string {