Swap: tell the user when a journal is locked out or failing; long names

- SwapManager queues user notices when a journal is held by another kte
  (this session's edits are then not journaled) and, once per gap, when
  journal writes fail. Editor shows them in the status line from its
  per-frame hook. Both went only to the error log, so crash recovery was
  silently off or incomplete.
- The fallback swap name '<basename>.<hash>.swp' exceeded NAME_MAX for
  basenames over ~233 bytes, so such files were never journaled; the
  basename is shortened (on a UTF-8 boundary) only in that case.
- Audit doc: sixth-round findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 868ee3fe2f
commit 78d78f6f54
5 changed files with 159 additions and 5 deletions
+77
View File
@@ -1441,3 +1441,80 @@ TEST(Audit_VisualLine_EditsKeepUtf8Whole)
ASSERT_EQ(h.Text(), std::string("a\nbcdef\n\xE4\xB8\x96\n\xE7\x95\x8C\n"));
}
}
// A session that cannot journal its buffer (another kte holds the journal,
// or writes fail) now says so in the status line; it used to go only to
// the error log, leaving crash recovery silently off.
TEST(Audit_Swap_FailuresReachTheUser)
{
TempDir d("swap_notice");
const std::string path = (d.path / "f.txt").string();
std::ofstream(path) << "base\n";
const std::string swp = kte::SwapManager::ComputeSwapPathForFilename(path);
std::filesystem::remove_all(swp);
// Session A holds the journal; session B is locked out.
Buffer a, b;
std::string err;
ASSERT_TRUE(a.OpenFromFile(path, err));
ASSERT_TRUE(b.OpenFromFile(path, err));
kte::SwapManager sa, sb;
sa.Attach(&a);
a.SetSwapRecorder(sa.RecorderFor(&a));
a.insert_text(0, 0, "A");
sa.Flush(&a);
ASSERT_EQ(sa.TakeUserNotice(), std::string());
sb.Attach(&b);
b.SetSwapRecorder(sb.RecorderFor(&b));
b.insert_text(0, 0, "B");
sb.Flush(&b);
ASSERT_TRUE(sb.TakeUserNotice().find("another kte") != std::string::npos);
ASSERT_EQ(sb.TakeUserNotice(), std::string());
b.SetSwapRecorder(nullptr);
sb.Detach(&b, false);
a.SetSwapRecorder(nullptr);
sa.Detach(&a, true);
// Writes fail: told once, not per keystroke.
std::filesystem::remove_all(swp);
std::filesystem::create_directories(swp);
Buffer c;
ASSERT_TRUE(c.OpenFromFile(path, err));
kte::SwapManager sc;
sc.Attach(&c);
c.SetSwapRecorder(sc.RecorderFor(&c));
c.insert_text(0, 0, "C");
c.insert_text(0, 1, "D");
sc.Flush(&c);
ASSERT_TRUE(sc.TakeUserNotice().find("journal write failed") != std::string::npos);
ASSERT_EQ(sc.TakeUserNotice(), std::string());
c.SetSwapRecorder(nullptr);
sc.Detach(&c, false);
std::filesystem::remove_all(swp);
}
// A file whose basename is too long for the encoded swap name fell back to
// "<basename>.<hash>.swp", which exceeded NAME_MAX: no journal at all.
TEST(Audit_Swap_LongBasenameStillJournaled)
{
TempDir d("swap_longname");
const std::string path = (d.path / (std::string(240, 'n') + ".txt")).string();
std::ofstream(path) << "base\n";
const std::string swp = kte::SwapManager::ComputeSwapPathForFilename(path);
ASSERT_TRUE(std::filesystem::path(swp).filename().string().size() <= 255);
std::filesystem::remove(swp);
Buffer a;
std::string err;
ASSERT_TRUE(a.OpenFromFile(path, err));
kte::SwapManager sm;
sm.Attach(&a);
a.SetSwapRecorder(sm.RecorderFor(&a));
a.insert_text(0, 0, "X");
sm.Flush(&a);
ASSERT_TRUE(std::filesystem::exists(swp));
a.SetSwapRecorder(nullptr);
sm.Detach(&a, true);
ASSERT_TRUE(!std::filesystem::exists(swp));
}