diff --git a/src/Makefile.am b/src/Makefile.am
index 1c533da..f56eddc 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -320,8 +320,8 @@ TEST_SH = \
 	$(NULL)
 EXTRA_DIST += $(TEST_SH)
 
-check_PROGRAMS = dumpkeys reflect-text-view reflect-vte mev table xticker vteconv
-TESTS = table vteconv $(TEST_SH)
+check_PROGRAMS = dumpkeys reflect-text-view reflect-vte mev table xticker vteconv vtestream-file
+TESTS = table vteconv vtestream-file $(TEST_SH)
 
 reflect_text_view_CPPFLAGS = -DUSE_TEXT_VIEW $(AM_CPPFLAGS)
 reflect_text_view_CFLAGS = $(VTE_CFLAGS) $(AM_CFLAGS)
@@ -392,6 +392,24 @@ table_LDADD = \
 	$(GLIB_LIBS) \
 	$(GOBJECT_LIBS)
 
+vtestream_file_SOURCES = \
+	vtestream-base.h \
+	vtestream-file.h \
+	vtestream.c \
+	vtestream.h \
+	vteutils.c \
+	vteutils.h \
+	$(NULL)
+vtestream_file_CPPFLAGS = \
+	-DVTESTREAM_MAIN \
+	$(AM_CPPFLAGS)
+vtestream_file_CFLAGS = \
+	$(GLIB_CFLAGS) \
+	$(AM_CFLAGS)
+vtestream_file_LDADD = \
+	$(GLIB_LIBS) \
+	$(GOBJECT_LIBS)
+
 vteconv_SOURCES = buffer.h debug.c debug.h vteconv.c vteconv.h
 vteconv_CPPFLAGS = -DVTECONV_MAIN $(AM_CPPFLAGS)
 vteconv_CFLAGS = $(VTE_CFLAGS) $(AM_CFLAGS)
diff --git a/src/vtestream-base.h b/src/vtestream-base.h
index ebe3eec..8b8ac6d 100644
--- a/src/vtestream-base.h
+++ b/src/vtestream-base.h
@@ -37,6 +37,7 @@ typedef struct _VteStreamClass {
 	gboolean (*read) (VteStream *stream, gsize offset, char *data, gsize len);
 	void (*truncate) (VteStream *stream, gsize offset);
 	void (*advance_tail) (VteStream *stream, gsize offset);
+	gsize (*tail) (VteStream *stream);
 	gsize (*head) (VteStream *stream);
 } VteStreamClass;
 
@@ -87,6 +88,12 @@ _vte_stream_advance_tail (VteStream *stream, gsize offset)
 }
 
 gsize
+_vte_stream_tail (VteStream *stream)
+{
+	return VTE_STREAM_GET_CLASS (stream)->tail (stream);
+}
+
+gsize
 _vte_stream_head (VteStream *stream)
 {
 	return VTE_STREAM_GET_CLASS (stream)->head (stream);
diff --git a/src/vtestream-file.h b/src/vtestream-file.h
index bf67005..74c3f35 100644
--- a/src/vtestream-file.h
+++ b/src/vtestream-file.h
@@ -36,39 +36,44 @@
  * 4. Two continuous regions, the first one preceding the second.
  *
  * Initially, data is simply written to the file, we're at state 1:
- * (A) abcdef
+ * (A) abcd
  *
  * Later the tail starts to advance too, but we're still in state 1:
- * (B) __cdefgh
+ * (B) ___de
  *
  * At some point, based on heuristics, we wrap to the beginning, into state 2:
- * (C) k___efghij
+ * (C) f__de
  *
  * The trivial case: If tail reaches EOF before head would bite tail, we're
  * back at state 1 and we can even truncate the file:
- * (D) klmnop____
- * (E) klmnop
+ * (D) fg___
+ * (E) fg
  *
- * Let's jump back to (C) to see the non-trivial case:
- * (C) k___efghij
+ * Let's write some more data and advance the tail to get back to a state
+ * equivalent to (C).
+ * (F) k__ij
  *
  * If head would bite tail, we continue appending at EOF, entering state 3:
- * (F) klmnofghijp
+ * (G) klmijno
  *
  * As tail keeps advancing, we're still in state 3:
- * (G) klmno__hijpqr
+ * (H) klm_jnop
  *
  * When tail finishes with the middle segment, we enter state 4:
- * (H) klmno_____pqrst
+ * (I) klm__nop
  *
  * Further advancing tail keeps us at state 4:
- * (I) __mno_____pqrstuvw
+ * (J) __m__nopqr
  *
  * When tail finishes with the first segment, we return to state 1:
- * (J) __________pqrstuvwxy
+ * (K) _____nopqr
  *
- * Based on the aforementioned heuristics, we probably soon switch to state 2:
- * (K) z___________rstuvwxy
+ * Depending on the aforementioned heuristics, we might stay in state 2:
+ * (L) _____nopqrs
+ *
+ * But probably we soon return to state 1:
+ * (M) ______opqrs
+ * (N) tu____opqrs
  *
  * and so on...
  */
@@ -81,6 +86,13 @@
 #include "vteutils.h"
 
 
+#ifndef VTESTREAM_MAIN
+#define VTE_FILE_STREAM_BLOCKSIZE 4096
+#else
+#define VTE_FILE_STREAM_BLOCKSIZE 4  /* Small block size for unit testing */
+#endif
+
+
 #ifndef HAVE_PREAD
 #define pread _pread
 static inline gsize
@@ -349,20 +361,25 @@ _vte_unbuffered_file_stream_read (VteStream *astream, gsize offset, char *data,
 	int segments = VTE_UNBUFFERED_FILE_STREAM_SEGMENTS(stream);
 	int i;
 
-	/* The caller expects out-of-bounds requests to return FALSE. */
-	if (G_UNLIKELY (offset + len <= stream->tail || offset >= stream->head))
-		return FALSE;
-
-	/* However, no request is partially inside-bounds and partially out-of-bounds. */
-	g_assert (offset >= stream->tail);
-	g_assert (offset + len <= stream->head);
+	/* Out of bounds request. */
+	if (G_UNLIKELY (offset < stream->tail || offset + len > stream->head)) {
+		/* If completely out of bounds, the caller expects a FALSE. */
+		if (offset + len <= stream->tail || offset >= stream->head)
+			return FALSE;
+		/* Partial out of bounds requests never happen. */
+		g_assert_not_reached();
+	}
 
+	if (len == 0)
+		return TRUE;
 	for (i = 0; i < segments; i++) {
 		if (offset >= stream->segment[i].st_head)
 			continue;
 		l = MIN(stream->segment[i].st_head - offset, len);
 		l = _file_read (&stream->file, data, l, offset + stream->segment[i].fd_tail - stream->segment[i].st_tail);
-		offset += l; data += l; len -= l; if (!len) return TRUE;
+		offset += l; data += l; len -= l;
+		if (len == 0)
+			return TRUE;
 	}
 
 	return FALSE;
@@ -423,9 +440,9 @@ _vte_unbuffered_file_stream_truncate (VteStream *astream, gsize offset)
 				break;
 			}
 		}
+		last_segment = VTE_UNBUFFERED_FILE_STREAM_SEGMENTS(stream) - 1;
+		stream->head = stream->segment[last_segment].st_head;
 	}
-	last_segment = VTE_UNBUFFERED_FILE_STREAM_SEGMENTS(stream) - 1;
-	stream->head = stream->segment[last_segment].st_head;
 }
 
 /*
@@ -459,8 +476,8 @@ _vte_unbuffered_file_stream_advance_tail (VteStream *astream, gsize offset)
 				g_assert_not_reached();
 				break;
 			case 2:
-			case 4:
 				stream->segment[0] = stream->segment[1];
+				_file_try_truncate (&stream->file, stream->segment[0].fd_head);
 				stream->state = 1;
 				break;
 			case 3:
@@ -468,6 +485,10 @@ _vte_unbuffered_file_stream_advance_tail (VteStream *astream, gsize offset)
 				stream->segment[1] = stream->segment[2];
 				stream->state = 4;
 				break;
+			case 4:
+				stream->segment[0] = stream->segment[1];
+				stream->state = 1;
+				break;
 			default:
 				g_assert_not_reached();
 				break;
@@ -479,6 +500,14 @@ _vte_unbuffered_file_stream_advance_tail (VteStream *astream, gsize offset)
 }
 
 static gsize
+_vte_unbuffered_file_stream_tail (VteStream *astream)
+{
+	VteUnbufferedFileStream *stream = (VteUnbufferedFileStream *) astream;
+
+	return stream->tail;
+}
+
+static gsize
 _vte_unbuffered_file_stream_head (VteStream *astream)
 {
 	VteUnbufferedFileStream *stream = (VteUnbufferedFileStream *) astream;
@@ -498,6 +527,7 @@ _vte_unbuffered_file_stream_class_init (VteUnbufferedFileStreamClass *klass)
 	klass->read = _vte_unbuffered_file_stream_read;
 	klass->truncate = _vte_unbuffered_file_stream_truncate;
 	klass->advance_tail = _vte_unbuffered_file_stream_advance_tail;
+	klass->tail = _vte_unbuffered_file_stream_tail;
 	klass->head = _vte_unbuffered_file_stream_head;
 }
 
@@ -511,6 +541,8 @@ _vte_unbuffered_file_stream_class_init (VteUnbufferedFileStreamClass *klass)
 typedef struct _VteFileStream {
 	VteUnbufferedFileStream parent;
 
+	gsize blocksize;
+
 	char *rbuf;
 	gsize rbuf_offset;
 
@@ -538,8 +570,9 @@ _vte_file_stream_init (VteFileStream *stream)
 {
 	_vte_unbuffered_file_stream_init ((VteUnbufferedFileStream *) stream);
 
-	stream->rbuf = g_malloc(4096);
-	stream->wbuf = g_malloc(4096);
+	stream->rbuf = g_malloc(VTE_FILE_STREAM_BLOCKSIZE);
+	stream->wbuf = g_malloc(VTE_FILE_STREAM_BLOCKSIZE);
+	stream->rbuf_offset = 1;  /* Aligned to multiple of 4096, hence 1 never matches, i.e. we'll get cache miss */
 }
 
 static void
@@ -573,11 +606,11 @@ _vte_file_stream_append (VteStream *astream, const char *data, gsize len)
 
 	stream->head += len;
 	while (len) {
-		gsize l = MIN(4096 - stream->wbuf_len, len);
+		gsize l = MIN(VTE_FILE_STREAM_BLOCKSIZE - stream->wbuf_len, len);
 		memcpy(stream->wbuf + stream->wbuf_len, data, l);
 		stream->wbuf_len += l; data += l; len -= l;
-		if (stream->wbuf_len == 4096) {
-			_vte_unbuffered_file_stream_append (astream, stream->wbuf, 4096);
+		if (stream->wbuf_len == VTE_FILE_STREAM_BLOCKSIZE) {
+			_vte_unbuffered_file_stream_append (astream, stream->wbuf, VTE_FILE_STREAM_BLOCKSIZE);
 			stream->wbuf_len = 0;
 		}
 	}
@@ -588,27 +621,29 @@ _vte_file_stream_read (VteStream *astream, gsize offset, char *data, gsize len)
 {
 	VteFileStream *stream = (VteFileStream *) astream;
 
-	/* The caller expects out-of-bounds requests to return FALSE. */
-	if (G_UNLIKELY (offset + len <= stream->tail || offset >= stream->head))
-		return FALSE;
-	/* However, no request is partially inside-bounds and partially out-of-bounds. */
-	g_assert (offset >= stream->tail);
-	g_assert (offset + len <= stream->head);
+	/* Out of bounds request. */
+	if (G_UNLIKELY (offset < stream->tail || offset + len > stream->head)) {
+		/* If completely out of bounds, the caller expects a FALSE. */
+		if (offset + len <= stream->tail || offset >= stream->head)
+			return FALSE;
+		/* Partial out of bounds requests never happen. */
+		g_assert_not_reached();
+	}
 
 	while (len && offset < stream->parent.head) {
-		gsize l = MIN(4096 - offset % 4096, len);
-		gsize offset_aligned = offset / 4096 * 4096;
+		gsize l = MIN(VTE_FILE_STREAM_BLOCKSIZE - offset % VTE_FILE_STREAM_BLOCKSIZE, len);
+		gsize offset_aligned = offset / VTE_FILE_STREAM_BLOCKSIZE * VTE_FILE_STREAM_BLOCKSIZE;
 		if (offset_aligned != stream->rbuf_offset) {
-			if (!_vte_unbuffered_file_stream_read (astream, offset_aligned, stream->rbuf, 4096))
+			if (!_vte_unbuffered_file_stream_read (astream, offset_aligned, stream->rbuf, VTE_FILE_STREAM_BLOCKSIZE))
 				return FALSE;
 			stream->rbuf_offset = offset_aligned;
 		}
-		memcpy(data, stream->rbuf + offset % 4096, l);
+		memcpy(data, stream->rbuf + offset % VTE_FILE_STREAM_BLOCKSIZE, l);
 		offset += l; data += l; len -= l;
 	}
 	if (len) {
-		g_assert(offset % 4096 + len <= stream->wbuf_len);
-		memcpy(data, stream->wbuf + offset % 4096, len);
+		g_assert(offset % VTE_FILE_STREAM_BLOCKSIZE + len <= stream->wbuf_len);
+		memcpy(data, stream->wbuf + offset % VTE_FILE_STREAM_BLOCKSIZE, len);
 	}
 	return TRUE;
 }
@@ -628,10 +663,10 @@ _vte_file_stream_truncate (VteStream *astream, gsize offset)
 		 * simplest local hack here that allows to leave the rest of
 		 * the code intact, that is, read back the new partial last
 		 * block to the write cache. */
-		gsize offset_aligned = offset / 4096 * 4096;
-		if (!_vte_unbuffered_file_stream_read (astream, offset_aligned, stream->wbuf, 4096)) {
+		gsize offset_aligned = offset / VTE_FILE_STREAM_BLOCKSIZE * VTE_FILE_STREAM_BLOCKSIZE;
+		if (!_vte_unbuffered_file_stream_read (astream, offset_aligned, stream->wbuf, VTE_FILE_STREAM_BLOCKSIZE)) {
 			/* what now? */
-			memset(stream->wbuf, 0, 4096);
+			memset(stream->wbuf, 0, VTE_FILE_STREAM_BLOCKSIZE);
 		}
 		_vte_unbuffered_file_stream_truncate (astream, offset_aligned);
 
@@ -640,7 +675,7 @@ _vte_file_stream_truncate (VteStream *astream, gsize offset)
 			stream->rbuf_offset = 1;
 		}
 	}
-	stream->wbuf_len = offset % 4096;
+	stream->wbuf_len = offset % VTE_FILE_STREAM_BLOCKSIZE;
 	stream->head = offset;
 }
 
@@ -654,15 +689,22 @@ _vte_file_stream_advance_tail (VteStream *astream, gsize offset)
 
 	stream->tail = offset;
 
-	if (offset < 4096) return;  // XXX
-	offset = offset / 4096 * 4096 - 4096;  // TODO why is it faster with that extra "-4096"?... because the stream
-	  // doesn't become empty and so we don't call ftruncate
+	// TODO FIXME this keeps resetting the stream which is very slow!!!
+	offset = offset / VTE_FILE_STREAM_BLOCKSIZE * VTE_FILE_STREAM_BLOCKSIZE;
 	if (offset < stream->parent.segment[0].st_tail) return;
 
 	_vte_unbuffered_file_stream_advance_tail (astream, offset);
 }
 
 static gsize
+_vte_file_stream_tail (VteStream *astream)
+{
+	VteFileStream *stream = (VteFileStream *) astream;
+
+	return stream->tail;
+}
+
+static gsize
 _vte_file_stream_head (VteStream *astream)
 {
 	VteFileStream *stream = (VteFileStream *) astream;
@@ -683,5 +725,472 @@ _vte_file_stream_class_init (VteFileStreamClass *klass)
 	klass->read = _vte_file_stream_read;
 	klass->truncate = _vte_file_stream_truncate;
 	klass->advance_tail = _vte_file_stream_advance_tail;
+	klass->tail = _vte_file_stream_tail;
 	klass->head = _vte_file_stream_head;
 }
+
+
+#ifdef VTESTREAM_MAIN
+
+static void
+assert_file_contents (int fd, const char *s)
+{
+	char buf[100];
+	ssize_t n = pread(fd, buf, 100, 0);
+	g_assert(n == (ssize_t) strlen(s));
+	g_assert(memcmp(buf, s, n) == 0);
+}
+
+static void
+assert_stream_contents (VteStream *astream, const char *s)
+{
+	char buf[100];
+	gsize tail = _vte_stream_tail(astream);
+	gsize head = _vte_stream_head(astream);
+	g_assert(head - tail == strlen(s));
+	g_assert(_vte_stream_read(astream, tail, buf, head - tail));
+	g_assert(memcmp(buf, s, head - tail) == 0);
+}
+
+/* Unbuffered: Test the example at the top of this file. */
+static void
+test_unbuffered_example (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	g_assert (s->state == 1 && s->tail == 0 && s->head == 0);
+	assert_stream_contents (as, "");
+
+	/* (A) */
+	_vte_stream_append (as, "abcd", 4);
+	assert_file_contents (s->file.fd, "abcd");
+	assert_stream_contents (as, "abcd");
+
+	/* (B) */
+	_vte_stream_append (as, "e", 1);
+	_vte_stream_advance_tail (as, 3);
+	g_assert (s->state == 1 && s->tail == 3 && s->head == 5);
+	assert_file_contents (s->file.fd, "abcde");
+	assert_stream_contents (as, "de");
+
+	/* (C) */
+	/* This relies on the exact current behavior of the heuristics to switch to state 2. */
+	_vte_stream_append (as, "f", 1);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 6);
+	assert_file_contents (s->file.fd, "fbcde");
+	assert_stream_contents (as, "def");
+
+	/* (D)-(E) */
+	_vte_stream_append (as, "g", 1);
+	_vte_stream_advance_tail (as, 4);
+	g_assert (s->state == 2 && s->tail == 4 && s->head == 7);
+	assert_file_contents (s->file.fd, "fgcde");
+	assert_stream_contents (as, "efg");
+
+	_vte_stream_advance_tail (as, 5);
+	g_assert (s->state == 1 && s->tail == 5 && s->head == 7);
+	assert_file_contents (s->file.fd, "fg");
+	assert_stream_contents (as, "fg");
+
+	/* (F) */
+	_vte_stream_append (as, "hij", 3);
+	_vte_stream_advance_tail (as, 8);
+	g_assert (s->state == 1 && s->tail == 8 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghij");
+	assert_stream_contents (as, "ij");
+
+	_vte_stream_append (as, "k", 1);
+	g_assert (s->state == 2 && s->tail == 8 && s->head == 11);
+	assert_file_contents (s->file.fd, "kghij");
+	assert_stream_contents (as, "ijk");
+
+	/* (G) */
+	_vte_stream_append (as, "lmno", 4);
+	g_assert (s->state == 3 && s->tail == 8 && s->head == 15);
+	assert_file_contents (s->file.fd, "klmijno");
+	assert_stream_contents (as, "ijklmno");
+
+	/* (H) */
+	_vte_stream_append (as, "p", 1);
+	_vte_stream_advance_tail (as, 9);
+	g_assert (s->state == 3 && s->tail == 9 && s->head == 16);
+	assert_file_contents (s->file.fd, "klmijnop");
+	assert_stream_contents (as, "jklmnop");
+
+	/* (I) */
+	_vte_stream_advance_tail (as, 10);
+	g_assert (s->state == 4 && s->tail == 10 && s->head == 16);
+	assert_file_contents (s->file.fd, "klmijnop");
+	assert_stream_contents (as, "klmnop");
+
+	/* (J) */
+	_vte_stream_append (as, "qr", 2);
+	_vte_stream_advance_tail (as, 12);
+	g_assert (s->state == 4 && s->tail == 12 && s->head == 18);
+	assert_file_contents (s->file.fd, "klmijnopqr");
+	assert_stream_contents (as, "mnopqr");
+
+	/* (K) */
+	_vte_stream_advance_tail (as, 13);
+	g_assert (s->state == 1 && s->tail == 13 && s->head == 18);
+	assert_file_contents (s->file.fd, "klmijnopqr");
+	assert_stream_contents (as, "nopqr");
+
+	/* (K) */
+	_vte_stream_advance_tail (as, 13);
+	g_assert (s->state == 1 && s->tail == 13 && s->head == 18);
+	assert_file_contents (s->file.fd, "klmijnopqr");
+	assert_stream_contents (as, "nopqr");
+
+	/* (L) */
+	_vte_stream_append (as, "s", 1);
+	g_assert (s->state == 1 && s->tail == 13 && s->head == 19);
+	assert_file_contents (s->file.fd, "klmijnopqrs");
+	assert_stream_contents (as, "nopqrs");
+
+	/* (M)-(N) */
+	_vte_stream_advance_tail (as, 14);
+	_vte_stream_append (as, "tu", 2);
+	g_assert (s->state == 2 && s->tail == 14 && s->head == 21);
+	assert_file_contents (s->file.fd, "tumijnopqrs");
+	assert_stream_contents (as, "opqrstu");
+
+	g_object_unref (as);
+}
+
+/* Unbuffered: Test append() 1->2, 2->3, 1->3, and truncate() 3->2, 2->1, 3->1 state changes */
+static void
+test_unbuffered_append_truncate_123 (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	/* Setup */
+	_vte_stream_append (as, "abcde", 5);
+	_vte_stream_advance_tail (as, 3);
+	g_assert (s->state == 1 && s->tail == 3 && s->head == 5);
+	assert_file_contents (s->file.fd, "abcde");
+	assert_stream_contents (as, "de");
+
+	/* Append 1 byte, state change 1 -> 2 */
+	_vte_stream_append (as, "f", 1);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 6);
+	assert_file_contents (s->file.fd, "fbcde");
+	assert_stream_contents (as, "def");
+
+	/* Append, stay in state 2 */
+	_vte_stream_append (as, "gh", 2);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 8);
+	assert_file_contents (s->file.fd, "fghde");
+	assert_stream_contents (as, "defgh");
+
+	/* Append 1 byte, state change 2 -> 3 */
+	_vte_stream_append (as, "i", 1);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 9);
+	assert_file_contents (s->file.fd, "fghdei");
+	assert_stream_contents (as, "defghi");
+
+	/* Append, stay in state 3 */
+	_vte_stream_append (as, "jk", 2);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 11);
+	assert_file_contents (s->file.fd, "fghdeijk");
+	assert_stream_contents (as, "defghijk");
+
+	/* Truncate, stay in state 3 */
+	_vte_stream_truncate (as, 9);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 9);
+	assert_file_contents (s->file.fd, "fghdei");
+	assert_stream_contents (as, "defghi");
+
+	/* Truncate 1 byte, state change 3 -> 2 */
+	_vte_stream_truncate (as, 8);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 8);
+	assert_file_contents (s->file.fd, "fghde");
+	assert_stream_contents (as, "defgh");
+
+	/* Truncate, stay in state 2 */
+	_vte_stream_truncate (as, 6);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 6);
+	assert_file_contents (s->file.fd, "fghde");
+	assert_stream_contents (as, "def");
+
+	/* Truncate 1 byte, state change 2 -> 1 */
+	_vte_stream_truncate (as, 5);
+	g_assert (s->state == 1 && s->tail == 3 && s->head == 5);
+	assert_file_contents (s->file.fd, "fghde");
+	assert_stream_contents (as, "de");
+
+	/* Append just as much to fill the gap, state change 1 -> 2 */
+	_vte_stream_append (as, "lmn", 3);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 8);
+	assert_file_contents (s->file.fd, "lmnde");
+	assert_stream_contents (as, "delmn");
+
+	/* Truncate, state change 2 -> 1 */
+	_vte_stream_truncate (as, 5);
+	g_assert (s->state == 1 && s->tail == 3 && s->head == 5);
+	assert_file_contents (s->file.fd, "lmnde");
+	assert_stream_contents (as, "de");
+
+	/* Append, state chagnge 1 -> 2 -> 3 */
+	_vte_stream_append (as, "opqr", 4);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 9);
+	assert_file_contents (s->file.fd, "opqder");
+	assert_stream_contents (as, "deopqr");
+
+	/* Truncate, state change 3 -> 2 -> 1 */
+	_vte_stream_truncate (as, 5);
+	g_assert (s->state == 1 && s->tail == 3 && s->head == 5);
+	assert_file_contents (s->file.fd, "opqde");
+	assert_stream_contents (as, "de");
+
+	g_object_unref (as);
+}
+
+/* Unbuffered: Test truncate() 4->1 state change */
+static void
+test_unbuffered_truncate_41 (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	/* Setup */
+	_vte_stream_append (as, "abcde", 5);
+	_vte_stream_advance_tail (as, 3);
+	_vte_stream_append (as, "fghij", 5);
+	_vte_stream_advance_tail (as, 6);
+	g_assert (s->state == 4 && s->tail == 6 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "ghij");
+
+	/* Truncate, stay in state 4 */
+	_vte_stream_truncate (as, 9);
+	g_assert (s->state == 4 && s->tail == 6 && s->head == 9);
+	assert_file_contents (s->file.fd, "fghdei");
+	assert_stream_contents (as, "ghi");
+
+	/* Truncate, state change 4 -> 1 */
+	_vte_stream_truncate (as, 8);
+	g_assert (s->state == 1 && s->tail == 6 && s->head == 8);
+	assert_file_contents (s->file.fd, "fgh");
+	assert_stream_contents (as, "gh");
+
+	g_object_unref (as);
+}
+
+/* Unbuffered: Test advance_tail() 2->1 state change */
+static void
+test_unbuffered_advancetail_21 (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	/* Setup */
+	_vte_stream_append (as, "abcde", 5);
+	_vte_stream_advance_tail (as, 3);
+	_vte_stream_append (as, "fg", 2);
+	g_assert (s->state == 2 && s->tail == 3 && s->head == 7);
+	assert_file_contents (s->file.fd, "fgcde");
+	assert_stream_contents (as, "defg");
+
+	/* Advance_tail, stay in state 2 */
+	_vte_stream_advance_tail (as, 4);
+	g_assert (s->state == 2 && s->tail == 4 && s->head == 7);
+	assert_file_contents (s->file.fd, "fgcde");
+	assert_stream_contents (as, "efg");
+
+	/* Advance_tail, state change 2 -> 1 */
+	_vte_stream_advance_tail (as, 5);
+	g_assert (s->state == 1 && s->tail == 5 && s->head == 7);
+	assert_file_contents (s->file.fd, "fg");
+	assert_stream_contents (as, "fg");
+
+	g_object_unref (as);
+}
+
+/* Unbuffered: Test advance_tail() 3->4, 4->1 state changes */
+static void
+test_unbuffered_advancetail_341 (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	/* Setup */
+	_vte_stream_append (as, "abcde", 5);
+	_vte_stream_advance_tail (as, 3);
+	_vte_stream_append (as, "fghij", 5);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "defghij");
+
+	/* Advance_tail, stay in state 3 */
+	_vte_stream_advance_tail (as, 4);
+	g_assert (s->state == 3 && s->tail == 4 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "efghij");
+
+	/* Advance_tail, state change 3 -> 4 */
+	_vte_stream_advance_tail (as, 5);
+	g_assert (s->state == 4 && s->tail == 5 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "fghij");
+
+	/* Advance_tail, stay in state 4 */
+	_vte_stream_advance_tail (as, 7);
+	g_assert (s->state == 4 && s->tail == 7 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "hij");
+
+	/* Advance_tail, state change 4 -> 1 */
+	_vte_stream_advance_tail (as, 8);
+	g_assert (s->state == 1 && s->tail == 8 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "ij");
+
+	g_object_unref (as);
+}
+
+/* Unbuffered: Test advance_tail() 3->1 state changes */
+static void
+test_unbuffered_advancetail_31 (void)
+{
+	VteStream *as = _vte_unbuffered_file_stream_new();
+	VteUnbufferedFileStream *s = (VteUnbufferedFileStream *) as;
+
+	/* Setup */
+	_vte_stream_append (as, "abcde", 5);
+	_vte_stream_advance_tail (as, 3);
+	_vte_stream_append (as, "fghij", 5);
+	g_assert (s->state == 3 && s->tail == 3 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "defghij");
+
+	/* Advance_tail, state change 3 -> 4 -> 1*/
+	_vte_stream_advance_tail (as, 8);
+	g_assert (s->state == 1 && s->tail == 8 && s->head == 10);
+	assert_file_contents (s->file.fd, "fghdeij");
+	assert_stream_contents (as, "ij");
+
+	g_object_unref (as);
+}
+
+static void
+test_buffer (void)
+{
+	char buf[4];
+
+	VteStream *as = _vte_file_stream_new();
+	VteFileStream *s = (VteFileStream *) as;
+
+	/* Append */
+	_vte_stream_append (as, "abc", 3);
+	g_assert (s->head == 3);
+	g_assert (s->parent.head == 0);
+	g_assert (s->parent.file.fd == -1);
+
+	_vte_stream_append (as, "d", 1);
+	g_assert (s->head == 4);
+	g_assert (s->parent.head == 4);
+	assert_file_contents (s->parent.file.fd, "abcd");
+	assert_stream_contents (as, "abcd");
+
+	_vte_stream_append (as, "efghij", 6);
+	g_assert (s->head == 10);
+	g_assert (s->parent.head == 8);
+	assert_file_contents (s->parent.file.fd, "abcdefgh");
+	assert_stream_contents (as, "abcdefghij");
+
+	_vte_stream_append (as, "klm", 3);
+	g_assert (s->head == 13);
+	g_assert (s->parent.head == 12);
+	assert_file_contents (s->parent.file.fd, "abcdefghijkl");
+	assert_stream_contents (as, "abcdefghijklm");
+
+	/* Truncate */
+	_vte_stream_truncate (as, 12);
+	g_assert (s->head == 12);
+	g_assert (s->parent.head == 12);
+	assert_file_contents (s->parent.file.fd, "abcdefghijkl");
+	assert_stream_contents (as, "abcdefghijkl");
+
+	_vte_stream_truncate (as, 9);
+	g_assert (s->head == 9);
+	g_assert (s->parent.head == 8);
+	assert_file_contents (s->parent.file.fd, "abcdefgh");
+	assert_stream_contents (as, "abcdefghi");
+
+	/* Test that the read cache is invalidated on truncate */
+	_vte_stream_read (as, 4, buf, 4);
+	g_assert (s->rbuf_offset == 4);
+	_vte_stream_truncate (as, 7);
+	g_assert (s->rbuf_offset == 1);
+	_vte_stream_append (as, "mn", 2);
+	_vte_stream_read (as, 4, buf, 4);
+	g_assert (s->rbuf_offset == 4);
+	g_assert (strncmp (buf, "efgm", 4) == 0);
+
+	/* Advance_tail */
+	_vte_stream_advance_tail (as, 1);
+	g_assert (s->tail == 1);
+	g_assert (s->parent.tail == 0);
+	assert_file_contents (s->parent.file.fd, "abcdefgm");
+	assert_stream_contents (as, "bcdefgmn");
+
+	_vte_stream_advance_tail (as, 3);
+	g_assert (s->tail == 3);
+	g_assert (s->parent.tail == 0);
+	assert_file_contents (s->parent.file.fd, "abcdefgm");
+	assert_stream_contents (as, "defgmn");
+
+	_vte_stream_advance_tail (as, 4);
+	g_assert (s->tail == 4);
+	g_assert (s->parent.tail == 4);
+	assert_file_contents (s->parent.file.fd, "abcdefgm");
+	assert_stream_contents (as, "efgmn");
+
+	/* Tail and head within the same block */
+	_vte_stream_advance_tail (as, 8);
+	g_assert (s->tail == 8);
+	g_assert (s->parent.tail == 8);
+	assert_stream_contents (as, "n");
+
+	_vte_stream_advance_tail (as, 9);
+	g_assert (s->tail == 9);
+	g_assert (s->parent.tail == 8);
+	assert_stream_contents (as, "");
+
+	_vte_stream_append (as, "op", 2);
+	g_assert (s->head == 11);
+	g_assert (s->parent.head == 8);
+	assert_stream_contents (as, "op");
+
+	_vte_stream_advance_tail (as, 10);
+	g_assert (s->tail == 10);
+	g_assert (s->parent.tail == 8);
+	assert_stream_contents (as, "p");
+
+	_vte_stream_append (as, "q", 1);
+	g_assert (s->head == 12);
+	g_assert (s->parent.head == 12);
+	assert_stream_contents (as, "pq");
+
+	g_object_unref (as);
+}
+
+int
+main (int argc, char **argv)
+{
+	test_unbuffered_example();
+	test_unbuffered_append_truncate_123();
+	test_unbuffered_truncate_41();
+	test_unbuffered_advancetail_21();
+	test_unbuffered_advancetail_341();
+	test_unbuffered_advancetail_31();
+
+	test_buffer();
+
+	return 0;
+}
+
+#endif /* VTESTREAM_MAIN */
diff --git a/src/vtestream.h b/src/vtestream.h
index 6d57d4a..d1b4172 100644
--- a/src/vtestream.h
+++ b/src/vtestream.h
@@ -33,6 +33,7 @@ void _vte_stream_append (VteStream *stream, const char *data, gsize len);
 gboolean _vte_stream_read (VteStream *stream, gsize offset, char *data, gsize len);
 void _vte_stream_truncate (VteStream *stream, gsize offset);
 void _vte_stream_advance_tail (VteStream *stream, gsize offset);
+gsize _vte_stream_tail (VteStream *stream);
 gsize _vte_stream_head (VteStream *stream);
 
 /* Various streams */
