diff --git a/t/expat_xs_coverage.t b/t/expat_xs_coverage.t
new file mode 100644
index 0000000..8200b6f
--- /dev/null
+++ b/t/expat_xs_coverage.t
@@ -0,0 +1,422 @@
+use strict;
+use warnings;
+use Test::More;
+use XML::Parser;
+use XML::Parser::Expat;
+
+# Tests targeting specific uncovered code paths in Expat.xs identified by gcov.
+# Baseline coverage: 93.19% — these tests exercise the remaining reachable gaps.
+
+plan tests => 26;
+
+# ===== skip_until with Char handler (suspend L1237 / resume L1278) =====
+# skip_until() calls suspend_callbacks() then resume_callbacks() after the
+# target index is reached. The Char handler branches in these functions are
+# only hit when a Char handler is registered during skip_until.
+{
+ my @chars;
+ my $xml = 'text1text2text3';
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ if ($el eq 'a') {
+ # Skip from 'a' to beyond 'b' — element_index of 'c' should be 4
+ $xp->skip_until(4);
+ }
+ },
+ Char => sub { push @chars, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ # 'text1' and 'text2' should be suppressed by skip_until,
+ # 'text3' should appear after resume
+ my $text = join('', @chars);
+ like($text, qr/text3/, 'skip_until + Char: text after resume point is delivered');
+ unlike($text, qr/text2/, 'skip_until + Char: text during skip is suppressed');
+}
+
+# ===== skip_until with CdataSection handlers (suspend L1253 / resume L1291) =====
+{
+ my @cdata_starts;
+ my $xml = '';
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ if ($el eq 'a') {
+ $xp->skip_until(4); # Skip past 'b'
+ }
+ },
+ CdataStart => sub { push @cdata_starts, 1 },
+ Char => sub { }, # suppress output
+ },
+ );
+ $p->parse($xml);
+ # The CDATA in should be skipped, the one in should fire
+ is(scalar @cdata_starts, 1, 'skip_until + CdataStart: only post-skip CDATA fires');
+}
+
+# ===== skip_until with Unparsed and Notation handlers (suspend L1259,1264 / resume L1295,1299) =====
+# DTD events fire before document body, so skip_until from Init skips them.
+{
+ my @notation_names;
+ my @unparsed_names;
+ my $xml = <<'XML';
+
+
+]>
+
+XML
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Init => sub { $_[0]->skip_until(1) },
+ Notation => sub { push @notation_names, $_[1] },
+ Unparsed => sub { push @unparsed_names, $_[1] },
+ Start => sub { },
+ },
+ );
+ $p->parse($xml);
+ # DTD declarations happen before any elements, so skip_until(1) should
+ # skip them. After resume, Notation/Unparsed are restored for future events.
+ # The key coverage goal: suspend_callbacks and resume_callbacks exercise
+ # the Notation and Unparsed branches simply by being registered.
+ pass('skip_until + Notation/Unparsed: suspend/resume completed without crash');
+}
+
+# ===== skip_until with ExternEnt handler (suspend L1268 / resume L1303) =====
+{
+ my @ext_calls;
+ my $xml = <<'XML';
+
+]>
+&ext;
+XML
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ if ($el eq 'a') {
+ $xp->skip_until(4); # Skip past 'b' and its entity ref
+ }
+ },
+ ExternEnt => sub {
+ push @ext_calls, 1;
+ return '';
+ },
+ },
+ );
+ $p->parse($xml);
+ # Entity in should be skipped
+ is(scalar @ext_calls, 0, 'skip_until + ExternEnt: entity ref during skip is suppressed');
+}
+
+# ===== RecognizedString with existing recstring reset (L2009) =====
+# When recognized_string is called and cbv->recstring already exists from a
+# previous call, the sv_setpvn("", 0) reset path is hit.
+{
+ my @recs;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub { push @recs, $_[0]->recognized_string() },
+ },
+ );
+ $p->parse('');
+ is(scalar @recs, 3, 'recstring reset: three Start events');
+ is($recs[1], '', 'recstring reset: second call correct after reset');
+ is($recs[2], '', 'recstring reset: third call correct after reset');
+}
+
+# ===== recString append path (L1227) =====
+# Multi-attribute start tags generate multiple default events, exercising
+# the sv_catpvn path where recstring already has content.
+{
+ my $rec;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub { $rec = $_[0]->recognized_string() if $_[1] eq 'e' },
+ },
+ );
+ $p->parse('');
+ like($rec, qr/^$/, 'recString append: multi-attr tag captured fully');
+ like($rec, qr/e="5"/, 'recString append: last attribute present');
+}
+
+# ===== SkipUntil early return (L2338) =====
+# When skip_until is called with an index <= current st_serial, it returns
+# immediately without suspending callbacks.
+{
+ my @starts;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ push @starts, $el;
+ if ($el eq 'b') {
+ $xp->skip_until(0); # Already past index 0 — no-op
+ }
+ },
+ },
+ );
+ $p->parse('');
+ ok(scalar(grep { $_ eq 'c' } @starts), 'skip_until(0): no-op, subsequent events still fire');
+}
+
+# ===== UnsetAllHandlers with namespaces (L2305) =====
+# When namespaces are enabled, UnsetAllHandlers (called from finish()) also
+# unsets namespace decl handlers. Must call finish() mid-parse from a handler.
+{
+ my @events;
+ my $finished;
+ my $xml = '';
+
+ my $p = XML::Parser->new(
+ Namespaces => 1,
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ push @events, $el;
+ if ($el eq 'a') {
+ $xp->finish;
+ $finished = 1;
+ }
+ },
+ },
+ );
+ $p->parse($xml);
+
+ ok($finished, 'UnsetAllHandlers ns: finish() called on namespace parser');
+ # 'b' should not appear because finish() stopped processing
+ ok(!grep({ $_ eq 'b' } @events), 'UnsetAllHandlers ns: no events after finish()');
+}
+
+# ===== ExternalEntityRef with PUBLIC id (L1015) =====
+# When an external entity has a PUBLIC id, the pubid push path is taken.
+{
+ my @ext_args;
+ my $xml = <<'XML';
+
+]>
+&ext;
+XML
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ ExternEnt => sub {
+ my ($xp, $base, $sysid, $pubid) = @_;
+ @ext_args = ($sysid, $pubid);
+ return '';
+ },
+ },
+ );
+ $p->parse($xml);
+ is($ext_args[0], 'sys.xml', 'ExternEnt pubid: sysid passed correctly');
+ is($ext_args[1], 'pub-id', 'ExternEnt pubid: pubid passed to handler');
+}
+
+# ===== pe_implicit DefaultCurrent for PE references (L989-994) =====
+{
+ my @defaults;
+ my @entities;
+
+ my $xml = <<'XML';
+
+%pe;
+]>
+
+XML
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Entity => sub { push @entities, $_[1] },
+ Default => sub { push @defaults, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ my $dtext = join('', @defaults);
+ like($dtext, qr/%pe;/, 'pe_implicit: PE reference routed to Default handler');
+}
+
+# ===== SetExternalEntityRefHandler pe_implicit fallback (L1710-1711) =====
+# When extent handler is cleared while pe_implicit is true, the C handler
+# is kept for PE support. Also tests L1005 (return 0 when no extent_sv).
+{
+ my @defaults;
+ my $xml = <<'XML';
+
+%pe;
+
+]>
+
+XML
+
+ my @attlists;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Attlist => sub {
+ my ($xp, $elname, $attname) = @_;
+ push @attlists, $attname;
+ $xp->setHandlers(ExternEnt => undef);
+ },
+ Default => sub { push @defaults, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ ok(scalar @attlists >= 1, 'pe_implicit fallback: Attlist handler fired');
+}
+
+# ===== OriginalString with NULL context (L2254) =====
+SKIP: {
+ my $has_context = eval {
+ my $p = XML::Parser->new(
+ Handlers => { Start => sub { $_[0]->original_string() } }
+ );
+ $p->parse('');
+ 1;
+ };
+
+ if ($has_context) {
+ skip 'expat has XML_CONTEXT_BYTES — cannot test NULL context path', 1;
+ }
+
+ my $orig;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub { $orig = $_[0]->original_string() },
+ },
+ );
+ $p->parse('');
+ is($orig, '', 'original_string: returns empty string without XML_CONTEXT_BYTES');
+}
+
+# ===== PositionContext with NULL context (L1911) =====
+SKIP: {
+ my $has_context = eval {
+ my $p = XML::Parser->new(
+ Handlers => { Start => sub { $_[0]->original_string() } }
+ );
+ $p->parse('');
+ 1;
+ };
+
+ if ($has_context) {
+ skip 'expat has XML_CONTEXT_BYTES — cannot test NULL PositionContext path', 1;
+ }
+
+ my @ctx;
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub { @ctx = $_[0]->position_in_context(0) },
+ },
+ );
+ $p->parse('');
+ is(scalar @ctx, 0, 'position_in_context: returns nothing without XML_CONTEXT_BYTES');
+}
+
+# ===== Do_External_Parse with string result (L2363-2368) =====
+{
+ my $xml = <<'XML';
+
+]>
+&ext;
+XML
+
+ my @chars;
+ my $p = XML::Parser->new(
+ Handlers => {
+ ExternEnt => sub { return 'hello' },
+ Char => sub { push @chars, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ my $text = join('', @chars);
+ like($text, qr/hello/, 'Do_External_Parse: string return parsed as XML');
+}
+
+# ===== ExternEnt no handler (L1004-1005) =====
+# Without an ExternEnt handler, the C callback isn't registered, so expat
+# handles the failure internally. This verifies the error path.
+{
+ my $xml = <<'XML';
+
+]>
+&ext;
+XML
+
+ eval {
+ my $p = XML::Parser->new();
+ $p->parse($xml);
+ };
+ like($@, qr/error/i, 'No ExternEnt handler: parse fails on external entity ref');
+}
+
+# ===== ExternEnt handler returning bad XML triggers error path (L1060-1066) =====
+# When Do_External_Parse croaks, the error is captured in errmsg.
+{
+ my $xml = <<'XML';
+
+]>
+&ext;
+XML
+
+ eval {
+ my $p = XML::Parser->new(
+ Handlers => {
+ ExternEnt => sub { return 'no close tag' },
+ },
+ );
+ $p->parse($xml);
+ };
+ like($@, qr/error|not well-formed|junk/i,
+ 'ExternEnt error path: bad XML from handler triggers parse error');
+}
+
+# ===== skip_until with Comment handler (suspend L1246 / resume L1286) =====
+{
+ my @comments;
+ my $xml = '';
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ $xp->skip_until(3) if $el eq 'a'; # Skip past first comment and
+ },
+ Comment => sub { push @comments, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ is(scalar @comments, 1, 'skip_until + Comment: only post-skip comment fires');
+ like($comments[0], qr/see this/, 'skip_until + Comment: correct comment delivered');
+}
+
+# ===== skip_until with Proc handler (suspend L1241 / resume L1282) =====
+{
+ my @procs;
+ my $xml = '';
+
+ my $p = XML::Parser->new(
+ Handlers => {
+ Start => sub {
+ my ($xp, $el) = @_;
+ $xp->skip_until(3) if $el eq 'a';
+ },
+ Proc => sub { push @procs, $_[1] },
+ },
+ );
+ $p->parse($xml);
+ is(scalar @procs, 1, 'skip_until + Proc: only post-skip PI fires');
+ is($procs[0], 'keep', 'skip_until + Proc: correct PI delivered');
+}