Files
natsnet/docs/plans/2026-02-26-phase7-plan.md
Joseph Doherty 7750b46f9f docs: Phase 7 implementation plan — 11 tasks, 10 sessions
Covers schema migration, small-module verification, 224 stub fills,
401 new unit tests, 2126 deferred server-integration tests, and
NatsServerBehaviorTests integration baseline.
2026-02-26 18:49:24 -05:00

43 KiB
Raw Blame History

Phase 7: Porting Verification — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.

Goal: Verify all ported .NET code by writing and running targeted unit tests, then mark the ~2,126 server-integration tests as deferred so the database reflects ground truth.

Architecture: Ten sequential sessions: schema migration → small-module verification → stub-filling sessions (opts, jwt, auth, signal/log) → new test writing sessions (stores, filestore, jetstream) → final deferred-marking and integration tests. Each session ends with a passing test run, DB update, and commit.

Tech Stack: .NET 10, xUnit 3, Shouldly, NSubstitute, SQLite (porting.db), PortTracker CLI


Reference: DB State Entering Phase 7

Category Count
Small module tests (complete) 114
Server module tests (complete) 205
Server module tests (stub) 224 — unit tests, bodies to be written
Server module tests (not_started → unit) 401 — new tests to write
Server module tests (not_started → deferred) 2,126 — server-integration, to be marked
thw benchmarks (not_started → n/a) 6 — performance only, not applicable

Reference: Key File Paths

Purpose Path
Go reference tests golang/nats-server/server/<file>_test.go
.NET unit test project dotnet/tests/ZB.MOM.NatsNet.Server.Tests/
.NET integration test project dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/
PortTracker CLI tools/NatsNet.PortTracker/
SQLite database porting.db

Reference: Useful Commands

# Run all unit tests
dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/

# Run tests by class
dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~<ClassName>"

# PortTracker: mark test complete
dotnet run --project tools/NatsNet.PortTracker -- test update <id> --status complete --db porting.db

# PortTracker: mark module verified
dotnet run --project tools/NatsNet.PortTracker -- module update <id> --status verified --db porting.db

# PortTracker: mark all features in module verified
dotnet run --project tools/NatsNet.PortTracker -- feature update 0 --status verified --all-in-module <module_id> --db porting.db

# PortTracker: report
dotnet run --project tools/NatsNet.PortTracker -- report summary --db porting.db

# SQLite bulk update (for batch operations)
sqlite3 porting.db "UPDATE unit_tests SET status='deferred' WHERE ..."

Task 1: Schema Migration — Add deferred Status

Files:

  • Modify: porting-schema.sql (update CHECK constraint)
  • Modify: porting.db (recreate unit_tests table via migration SQL)

Step 1: Verify current constraint

sqlite3 porting.db ".schema unit_tests"

Expected: shows CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a')).

Step 2: Run migration SQL

sqlite3 porting.db << 'EOF'
BEGIN;

CREATE TABLE unit_tests_new (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    module_id INTEGER NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
    feature_id INTEGER REFERENCES features(id) ON DELETE SET NULL,
    name TEXT NOT NULL,
    description TEXT,
    go_file TEXT,
    go_class TEXT,
    go_method TEXT,
    go_line_number INTEGER,
    go_line_count INTEGER,
    status TEXT NOT NULL DEFAULT 'not_started'
        CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a', 'deferred')),
    dotnet_project TEXT,
    dotnet_class TEXT,
    dotnet_method TEXT,
    notes TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO unit_tests_new SELECT * FROM unit_tests;
DROP TABLE unit_tests;
ALTER TABLE unit_tests_new RENAME TO unit_tests;

-- Recreate indexes
CREATE INDEX IF NOT EXISTS idx_unit_tests_module ON unit_tests(module_id);
CREATE INDEX IF NOT EXISTS idx_unit_tests_feature ON unit_tests(feature_id);
CREATE INDEX IF NOT EXISTS idx_unit_tests_status ON unit_tests(status);

-- Recreate trigger
CREATE TRIGGER IF NOT EXISTS trg_unit_tests_updated AFTER UPDATE ON unit_tests
BEGIN
    UPDATE unit_tests SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

COMMIT;
EOF

Step 3: Verify migration

sqlite3 porting.db ".schema unit_tests"
# Confirm 'deferred' is in the CHECK constraint

sqlite3 porting.db "SELECT COUNT(*) FROM unit_tests;"
# Confirm row count matches original (3257)

# Verify existing data is intact
sqlite3 porting.db "SELECT status, COUNT(*) FROM unit_tests GROUP BY status;"

Expected status counts:

  • complete: 319
  • n_a: 181
  • not_started: 2533
  • stub: 224

Step 4: Update porting-schema.sql

Edit porting-schema.sql to add 'deferred' to the unit_tests CHECK constraint:

-- Find this line:
    status TEXT NOT NULL DEFAULT 'not_started'
        CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a')),

-- Change to:
    status TEXT NOT NULL DEFAULT 'not_started'
        CHECK (status IN ('not_started', 'stub', 'complete', 'verified', 'n_a', 'deferred')),

Step 5: Commit

git add porting-schema.sql porting.db
git commit -m "feat(p7-01): add 'deferred' status to unit_tests schema"

Task 2: P7-01 — Small Module Verification

Modules to verify: ats (id=1), avl (id=2), certidp (id=3), certstore (id=4), elastic (id=5), gsl (id=6), pse (id=7), stree (id=9), sysmem (id=10), thw (id=11), tpm (id=12)

Test counts: 114 complete tests across all small modules. 6 thw benchmarks → mark n_a.

Step 1: Run all small module tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~ZB.MOM.NatsNet.Server.Tests.Internal|FullyQualifiedName~ZB.MOM.NatsNet.Server.Tests.Auth.CertificateIdentityProvider|FullyQualifiedName~ZB.MOM.NatsNet.Server.Tests.Auth.TpmKeyProvider|FullyQualifiedName~TimeHashWheelTests|FullyQualifiedName~SubjectTreeTests|FullyQualifiedName~GenericSubjectListTests|FullyQualifiedName~ProcessStatsProviderTests|FullyQualifiedName~AccessTimeServiceTests"

Or run all unit tests and verify none fail:

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/

Expected: all existing tests pass (currently ~635 tests).

Step 2: Mark thw benchmarks as n/a

The 6 thw benchmarks (IDs 32503255) are Go benchmark functions, not correctness tests. Mark n_a:

for id in 3250 3251 3252 3253 3254 3255; do
  dotnet run --project tools/NatsNet.PortTracker -- test update $id --status n_a --db porting.db
done

Step 3: Mark all small module tests as verified

All complete tests in small modules (114 tests) pass — mark them verified. Use direct SQL for efficiency:

sqlite3 porting.db "
UPDATE unit_tests
SET status = 'verified'
WHERE status = 'complete' AND module_id IN (1,2,3,4,5,6,7,9,10,11,12);"

Verify:

sqlite3 porting.db "SELECT module_id, status, COUNT(*) FROM unit_tests WHERE module_id IN (1,2,3,4,5,6,7,9,10,11,12) GROUP BY module_id, status;"

Step 4: Mark all features in small modules as verified

for mod_id in 1 2 3 4 5 6 7 9 10 11 12; do
  dotnet run --project tools/NatsNet.PortTracker -- feature update 0 --status verified --all-in-module $mod_id --db porting.db
done

Step 5: Mark all small modules as verified

for mod_id in 1 2 3 4 5 6 7 9 10 11 12; do
  dotnet run --project tools/NatsNet.PortTracker -- module update $mod_id --status verified --db porting.db
done

Step 6: Verify and commit

dotnet run --project tools/NatsNet.PortTracker -- report summary --db porting.db
./reports/generate-report.sh
git add -A
git commit -m "feat(p7-01): verify 11 small modules (114 tests), mark thw benchmarks n/a"

Task 3: P7-02 — Opts Stubs (77 tests)

Go source: golang/nats-server/server/opts_test.go Target .NET file: dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs DB IDs: 25142597 (77 stubs in server/opts_test.go)

These test config file parsing and option binding. The Go tests at these line numbers use ProcessConfigFile, MergeOptions, and SetBaselineOptions — all of which are ported in Config/ServerOptionsConfiguration.cs and ServerOptions.cs.

Step 1: Read existing file and Go reference

# View existing ServerOptionsTests.cs (333 lines, 9 complete tests already written)
cat dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs

# List all 77 stubs from DB with Go line numbers
sqlite3 porting.db "
SELECT id, go_method, go_line_number, dotnet_method
FROM unit_tests
WHERE module_id=8 AND go_file='server/opts_test.go' AND status='stub'
ORDER BY go_line_number;"

Step 2: Classify stubs

Before writing tests, check each Go test to determine if it requires a running server:

  • Tests using RunServer() or RunBasicJetStreamServer() → mark deferred instead
  • Tests that only call ProcessConfigFile(), MergeOptions(), SetBaseline(), or validate option structs → write as unit test

Expected classification: Most opts stubs are config-parsing tests (unit). Some may be server-integration (mark deferred).

For each stub that needs a running server, run:

dotnet run --project tools/NatsNet.PortTracker -- test update <id> --status deferred --db porting.db

Step 3: Write test bodies

For each remaining stub, add a test method to ServerOptionsTests.cs.

Pattern for a config file test:

/// <summary>
/// Mirrors TestConfigFile — verify ProcessConfigFile loads host/port from JSON.
/// </summary>
[Fact] // T:2514
public void ConfigFile_ShouldSucceed()
{
    var json = """{"host":"localhost","port":4222}""";
    var tmp = Path.GetTempFileName() + ".json";
    File.WriteAllText(tmp, json);
    try
    {
        var opts = ServerOptionsConfiguration.ProcessConfigFile(tmp);
        opts.Host.ShouldBe("localhost");
        opts.Port.ShouldBe(4222);
    }
    finally { File.Delete(tmp); }
}

Pattern for a flag-override test:

/// <summary>
/// Mirrors TestRouteFlagOverride — flag opts override file opts.
/// </summary>
[Fact] // T:2517
public void RouteFlagOverride_ShouldSucceed()
{
    var fileOpts = new ServerOptions { Routes = ["nats://server1:5222"] };
    var flagOpts = new ServerOptions { Routes = ["nats://server2:5222"] };
    fileOpts.MergeOptions(flagOpts);
    fileOpts.Routes.ShouldContain("nats://server2:5222");
}

For Go tests that use config files, create a temp file with JSON content matching what the Go test uses, call ProcessConfigFile, and assert the resulting ServerOptions fields.

Step 4: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~ServerOptionsTests"

Fix any failures by comparing Go reference at the line numbers from Step 1.

Step 5: Mark complete in DB

For each test whose body was written and is passing:

dotnet run --project tools/NatsNet.PortTracker -- test update <id> --status complete --db porting.db

Or in bulk via SQLite if all 77 passed:

sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE module_id=8 AND go_file='server/opts_test.go' AND status='stub';"

Step 6: Run full suite and commit

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/
./reports/generate-report.sh
git add dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs porting.db reports/
git commit -m "feat(p7-02): fill opts_test.go stubs (77 tests) — ServerOptionsTests"

Task 4: P7-03 — JWT Stubs (88 tests)

Go source: golang/nats-server/server/jwt_test.go Target .NET file: dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs DB IDs: 18091896 (88 stubs)

These test JWT decode, validate, and claims extraction. The Go tests at these line numbers test pure JWT functions in jwt.go — they do NOT require a running server.

Step 1: List stubs

sqlite3 porting.db "
SELECT id, go_method, go_line_number, dotnet_method
FROM unit_tests
WHERE module_id=8 AND go_file='server/jwt_test.go' AND status='stub'
ORDER BY go_line_number;"

Step 2: Read existing test file

cat dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs

The file has complete tests for JwtPrefix, WipeSlice, ValidateSrc, etc. Stubs cover the remaining JWT functions.

Step 3: Write test bodies

Look up each Go test by line number in golang/nats-server/server/jwt_test.go. Write the corresponding .NET test method. The .NET JWT functions live in Auth/JwtProcessor.cs.

Pattern for a claims decode test:

/// <summary>
/// Mirrors TestJwtHeader — verify header prefix detection.
/// </summary>
[Fact] // T:1809
public void JwtHeader_ValidJwt_ReturnsTrue()
{
    var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.payload.sig";
    JwtProcessor.IsJwt(token).ShouldBeTrue();
}

Pattern for a validation error test:

/// <summary>
/// Mirrors TestDecodeAccountClaims_InvalidToken.
/// </summary>
[Fact] // T:1820
public void DecodeAccountClaims_InvalidToken_ReturnsError()
{
    var (claims, err) = JwtProcessor.DecodeAccountClaims("notajwt");
    claims.ShouldBeNull();
    err.ShouldNotBeNull();
}

Step 4: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~JwtProcessorTests"

Step 5: Mark complete and commit

sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE module_id=8 AND go_file='server/jwt_test.go' AND status='stub';"

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/
./reports/generate-report.sh
git add dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs porting.db reports/
git commit -m "feat(p7-03): fill jwt_test.go stubs (88 tests) — JwtProcessorTests"

Task 5: P7-04 — Auth & Config-Check Stubs (40 tests)

Go sources:

  • golang/nats-server/server/auth_test.go (6 stubs, IDs 147153, class AuthHandlerTests)
  • golang/nats-server/server/auth_callout_test.go (31 stubs, IDs 111141, class AuthCalloutTests)
  • golang/nats-server/server/config_check_test.go (3 stubs, IDs 271273, class ConfigCheckTests)

Target .NET files:

  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthHandlerTests.cs (add 6 methods)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthCalloutTests.cs (create or add 31 methods)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Config/ConfigCheckTests.cs (create 3 methods)

Step 1: List stubs per file

sqlite3 porting.db "
SELECT id, go_file, go_method, go_line_number, dotnet_class, dotnet_method
FROM unit_tests
WHERE id IN (111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,
             147,148,149,150,151,152,153,
             271,272,273)
ORDER BY go_file, go_line_number;"

Step 2: Auth handler stubs (6 tests)

Read existing AuthHandlerTests.cs, then add these 6 test methods:

  • T:147 TestUserUnknownAllowedConnectionType → test that passing an unrecognized connection type string to ValidateAllowedConnections returns an error
  • T:149 TestNoAuthUser → test that ProcessClientOrLeafAuthentication with no auth configured returns true when AuthRequired=false
  • T:150 TestNoAuthUserNkey → test that an NKey user with valid credentials authenticates when NKey list has one entry
  • T:151 TestUserConnectionDeadline → test User.ConnectionDeadline parsing from Unix timestamp in claims
  • T:152 TestNoAuthUserNoConnectProto → test that missing connect protocol fields get default handling
  • T:153 TestAuthProxyRequired → test that GetAuthErrClosedState(new AuthProxyRequiredException()) returns ClosedState.ProxyRequired

Pattern:

/// <summary>
/// Mirrors TestAuthProxyRequired.
/// </summary>
[Fact] // T:153
public void AuthProxyRequired_ShouldReturnProxyRequiredState()
{
    var state = AuthHandler.GetAuthErrClosedState(new AuthProxyRequiredException("proxy required"));
    state.ShouldBe(ClosedState.ProxyRequired);
}

Step 3: Auth callout stubs (31 tests, class AuthCalloutTests)

Important: Check Go tests at lines 212, 329, etc. in auth_callout_test.go. Most use RunBasicJetStreamServer(). If they do, mark them deferred instead:

dotnet run --project tools/NatsNet.PortTracker -- test update <id> --status deferred --db porting.db

For those that test pure helper types/functions (e.g., FillClientInfo, FillConnectOpts, serialization), write the unit test body.

Check which file AuthCalloutTests lives in:

grep -rn "AuthCalloutTests\|class AuthCallout" dotnet/tests/ | grep -v obj

If the file doesn't exist, create dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthCalloutTests.cs:

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;

namespace ZB.MOM.NatsNet.Server.Tests.Auth;

/// <summary>
/// Tests for AuthCallout helper types.
/// Mirrors type/helper tests from server/auth_callout_test.go.
/// </summary>
public class AuthCalloutTests
{
    // test methods here
}

Step 4: Config check stubs (3 tests, class ConfigCheckTests)

Go tests at lines corresponding to IDs 271273:

  • T:271 TestConfigCheck → test that a valid config passes validation
  • T:272 TestConfigCheckIncludes → test that includes are resolved
  • T:273 TestConfigCheckMultipleErrors → test that multiple validation errors are all reported

Create dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Config/ConfigCheckTests.cs:

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Config;

namespace ZB.MOM.NatsNet.Server.Tests.Config;

/// <summary>
/// Tests for configuration validation.
/// Mirrors server/config_check_test.go.
/// </summary>
public class ConfigCheckTests
{
    // test methods here
}

Step 5: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~AuthHandlerTests|FullyQualifiedName~AuthCalloutTests|FullyQualifiedName~ConfigCheckTests"

Step 6: Mark complete (or deferred) and commit

For each test marked complete:

dotnet run --project tools/NatsNet.PortTracker -- test update <id> --status complete --db porting.db
dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/
./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-04): fill auth & config_check stubs — AuthHandlerTests, AuthCalloutTests, ConfigCheckTests"

Task 6: P7-05 — Signal & Log Stubs (19 tests)

Go sources:

  • golang/nats-server/server/signal_test.go (16 stubs, IDs 29132928, class SignalHandlerTests)
  • golang/nats-server/server/log_test.go (3 stubs, IDs 20202022, class ServerLoggerTests)

Target .NET files:

  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs (add 16 methods)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs (create 3 methods)

Step 1: List stubs

sqlite3 porting.db "
SELECT id, go_file, go_method, go_line_number, dotnet_method
FROM unit_tests WHERE id BETWEEN 2913 AND 2928
ORDER BY go_line_number;"

sqlite3 porting.db "
SELECT id, go_file, go_method, go_line_number, dotnet_method
FROM unit_tests WHERE id IN (2020, 2021, 2022);"

Step 2: Signal stubs (16 tests)

Most signal tests involve ProcessSignal(command, pidExpression). Key tests:

  • T:29132928 cover: multi-process glob, pgrep errors, resolve single process, invalid command/PID, sending QUIT/TERM/Reopen/Reload to specific PID, lame duck mode, interrupt/term exit codes.

Read each Go test at the given line in golang/nats-server/server/signal_test.go. For tests that start a real server or spawn processes, mark deferred.

Pattern for pure logic tests:

/// <summary>
/// Mirrors TestProcessSignalInvalidCommand.
/// </summary>
[Fact] // T:2919
public void ProcessSignalInvalidCommand_ShouldReturnError()
{
    var err = SignalHandler.ProcessSignal((ServerCommand)99, "0");
    err.ShouldNotBeNull();
}

Add test methods to the existing SignalHandlerTests.cs file.

Step 3: Log stubs (3 tests)

The 3 stubs test password/token masking in protocol trace output:

  • T:2020 TestNoPasswordsFromConnectTrace — connect string with pass=secret should have password redacted in trace
  • T:2021 TestRemovePassFromTraceRemovePassFromTrace("... pass: secret ...") returns string without the password
  • T:2022 TestRemoveAuthTokenFromTraceRemoveAuthTokenFromTrace("... auth_token: tok ...") removes the token

Create dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs:

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server.Tests.Internal;

/// <summary>
/// Tests for password/token masking in server logging.
/// Mirrors server/log_test.go tests for trace sanitization.
/// </summary>
public class ServerLoggerTests
{
    [Fact] // T:2020
    public void NoPasswordsFromConnectTrace_ShouldRedactPass()
    {
        // See golang/nats-server/server/log_test.go:TestNoPasswordsFromConnectTrace
        // Call the relevant NatsServer or ServerLogging method that strips passwords from CONNECT traces
        // Assert the returned string does not contain the literal password
    }

    [Fact] // T:2021
    public void RemovePassFromTrace_ShouldRemovePassword()
    {
        var input = """{"verbose":false,"pedantic":false,"user":"derek","pass":"s3cr3t!","tls_required":false,"name":""}""";
        var result = ServerLogging.RemovePassFromTrace(input);
        result.ShouldNotContain("s3cr3t!");
        result.ShouldContain("\"pass\":\"\"");
    }

    [Fact] // T:2022
    public void RemoveAuthTokenFromTrace_ShouldRemoveToken()
    {
        var input = """{"verbose":false,"auth_token":"mytoken","tls_required":false}""";
        var result = ServerLogging.RemoveAuthTokenFromTrace(input);
        result.ShouldNotContain("mytoken");
        result.ShouldContain("\"auth_token\":\"\"");
    }
}

Check that ServerLogging.RemovePassFromTrace and RemoveAuthTokenFromTrace exist. If not, add them to Internal/ServerLogging.cs by reading the Go source in server/log.go.

Step 4: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~SignalHandlerTests|FullyQualifiedName~ServerLoggerTests"

Step 5: Mark complete and commit

# Mark signal stubs complete (adjust IDs for any that were deferred instead)
sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE id BETWEEN 2913 AND 2928 AND status='stub';"

for id in 2020 2021 2022; do
  dotnet run --project tools/NatsNet.PortTracker -- test update $id --status complete --db porting.db
done

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/
./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-05): fill signal & log stubs (19 tests) — SignalHandlerTests, ServerLoggerTests"

Task 7: P7-06 — Memory Store & Store Interface Tests (58 tests)

Go sources:

  • golang/nats-server/server/memstore_test.go (41 tests, IDs 20232063)
  • golang/nats-server/server/store_test.go (17 tests, IDs 29412957)

Target .NET files:

  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamMemoryStoreTests.cs (create, 41 tests)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StorageEngineTests.cs (create, 17 tests)

Important: These test the in-memory JetStream message store. The Go memstore.go is a pure data structure with no server dependency. Verify the .NET port exists before writing tests:

find dotnet/src -name "*.cs" | xargs grep -l "MemStore\|MemoryStore\|MsgStore" | grep -v obj

Step 1: Understand memstore interface

Read the Go store interface at the top of golang/nats-server/server/store.go. The .NET equivalent should implement a similar IMsgStore interface. If it hasn't been ported yet, note the missing implementation and write tests that call methods as they should exist.

Step 2: Classify memstore tests

From golang/nats-server/server/memstore_test.go, check each test:

  • Tests using NewMemStore() only → unit tests (write .NET equivalent)
  • Tests using RunServer() or requiring cluster → mark deferred

Step 3: Create JetStreamMemoryStoreTests.cs

// Copyright 2018-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server.JetStream;

namespace ZB.MOM.NatsNet.Server.Tests.JetStream;

/// <summary>
/// Tests for the in-memory JetStream message store.
/// Mirrors server/memstore_test.go.
/// </summary>
public class JetStreamMemoryStoreTests
{
    [Fact] // T:2023
    public void MemStoreBasics_ShouldStoreAndRetrieve()
    {
        // NewMemStore() → store message → retrieve → verify payload matches
        // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreBasics
    }

    // ... remaining test methods
}

Step 4: Create StorageEngineTests.cs

// Copyright 2018-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server.JetStream;

namespace ZB.MOM.NatsNet.Server.Tests.JetStream;

/// <summary>
/// Tests for the JetStream store interface contracts.
/// Mirrors server/store_test.go.
/// </summary>
public class StorageEngineTests
{
    // test methods
}

Step 5: Map tests in DB

For each new test method, update the DB to mark it complete and fill in the dotnet_method:

dotnet run --project tools/NatsNet.PortTracker -- test update <id> \
  --status complete --db porting.db

# Also update dotnet_method if not already set:
dotnet run --project tools/NatsNet.PortTracker -- test map <id> \
  --class JetStreamMemoryStoreTests \
  --method <MethodName> --db porting.db

Step 6: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~JetStreamMemoryStoreTests|FullyQualifiedName~StorageEngineTests"
dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/

Step 7: Commit

./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-06): port memstore & store interface tests (58 tests)"

Task 8: P7-07 — File Store Unit Tests, First Half (~125 tests)

Go source: golang/nats-server/server/filestore_test.go Target .NET file: dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamFileStoreTests.cs DB IDs: 351474 (first ~125 of 249 total, server/filestore_test.go)

Step 1: List first-half IDs and Go line numbers

sqlite3 porting.db "
SELECT id, go_method, go_line_number, dotnet_method
FROM unit_tests
WHERE module_id=8 AND go_file='server/filestore_test.go'
ORDER BY go_line_number
LIMIT 125;"

The split point for P7-07 vs P7-08 is approximately test ID 474 (the first 125 tests by Go line order).

Step 2: Verify filestore.go is ported

find dotnet/src -name "*.cs" | xargs grep -l "FileStore\|fileStore" | grep -v obj

If FileStore is not yet ported, this task must port the implementation first, then write tests. Check Phase 6 session notes for filestore porting status.

Step 3: Classify file store tests

From golang/nats-server/server/filestore_test.go, each test that uses newFileStore() or newFileStoreWithCreated() is a unit test. Tests that use RunServer() are deferred.

Step 4: Create JetStreamFileStoreTests.cs (first half)

// Copyright 2018-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using Shouldly;
using ZB.MOM.NatsNet.Server.JetStream;

namespace ZB.MOM.NatsNet.Server.Tests.JetStream;

/// <summary>
/// Unit tests for JetStream file store read/write/purge operations.
/// Mirrors server/filestore_test.go (first half).
/// </summary>
public class JetStreamFileStoreTests
{
    private static string TempDir() => Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

    [Fact] // T:351
    public void FileStoreBasics_ShouldStoreAndRetrieve()
    {
        // newFileStore(config) → store message → load → verify
        // Reference: golang/nats-server/server/filestore_test.go:TestFileStoreBasics
    }

    // ... remaining first-half tests
}

Step 5: Run, mark, commit

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~JetStreamFileStoreTests"

# Mark first-half tests complete via SQLite
sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE module_id=8 AND go_file='server/filestore_test.go' AND status='not_started'
AND id <= 474;"  -- adjust ID boundary based on actual split

./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-07): port filestore tests first half (~125 tests)"

Task 9: P7-08 — File Store Unit Tests, Second Half (~124 tests)

Go source: golang/nats-server/server/filestore_test.go Target .NET file: dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamFileStoreTests.cs DB IDs: 475599 (remaining ~124 of 249 total)

Step 1: List second-half IDs

sqlite3 porting.db "
SELECT id, go_method, go_line_number, dotnet_method
FROM unit_tests
WHERE module_id=8 AND go_file='server/filestore_test.go' AND status='not_started'
ORDER BY go_line_number;"

Step 2: Add remaining test methods

Continue adding methods to JetStreamFileStoreTests.cs for IDs 475599.

Step 3: Run, mark, commit

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~JetStreamFileStoreTests"

sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE module_id=8 AND go_file='server/filestore_test.go' AND status='not_started';"

./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-08): port filestore tests second half (~124 tests)"

Task 10: P7-09 — JetStream Unit Tests (94 tests)

Go sources:

  • golang/nats-server/server/jetstream_errors_test.go (4 tests, IDs 13811384)
  • golang/nats-server/server/jetstream_versioning_test.go (18 tests, IDs 17911808)
  • golang/nats-server/server/jetstream_batching_test.go (29 tests, IDs 716744)
  • golang/nats-server/server/dirstore_test.go (12 tests, IDs 285296)
  • golang/nats-server/server/accounts_test.go (31 tests, IDs 80110)

Target .NET files:

  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs (create)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamVersioningTests.cs (create)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamBatchingTests.cs (create)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/DirectoryStoreTests.cs (create)
  • dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Accounts/AccountTests.cs (add 31 methods to existing)

Step 1: List all IDs

sqlite3 porting.db "
SELECT id, go_file, go_method, go_line_number, dotnet_class, dotnet_method
FROM unit_tests
WHERE id IN (
  SELECT id FROM unit_tests WHERE go_file='server/jetstream_errors_test.go' AND status='not_started'
  UNION SELECT id FROM unit_tests WHERE go_file='server/jetstream_versioning_test.go' AND status='not_started'
  UNION SELECT id FROM unit_tests WHERE go_file='server/jetstream_batching_test.go' AND status='not_started'
  UNION SELECT id FROM unit_tests WHERE go_file='server/dirstore_test.go' AND status='not_started'
  UNION SELECT id FROM unit_tests WHERE go_file='server/accounts_test.go' AND status='not_started'
)
ORDER BY go_file, go_line_number;"

Step 2: JetStream errors (4 tests)

These test error type checking — pure type assertions:

// JetStreamErrorsTests.cs
[Fact] // T:1381
public void IsApiError_WithApiError_ReturnsTrue()
{
    var err = new ApiError(404, "not found");
    JetStreamErrors.IsApiError(err).ShouldBeTrue();
}

Step 3: JetStream versioning (18 tests)

These test version compatibility checks:

// JetStreamVersioningTests.cs
[Fact] // T:1791
public void ServerVersion_BelowMinimum_ReturnsError()
{
    var err = JetStreamVersioning.CheckServerVersion("2.1.0", "2.2.0");
    err.ShouldNotBeNull();
}

Step 4: JetStream batching (29 tests)

These test batch message accumulation logic (pure data structure, no server):

// JetStreamBatchingTests.cs
[Fact] // T:716
public void BatchBasics_ShouldAccumulateMessages()
{
    // Reference: golang/nats-server/server/jetstream_batching_test.go
}

Step 5: Directory store (12 tests)

These test the JWT directory store (reading/writing JWT files):

// DirectoryStoreTests.cs
[Fact] // T:285
public void DirStore_StoreAndFetch_ShouldRoundtrip()
{
    var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
    Directory.CreateDirectory(dir);
    try
    {
        // create DirStore, store a JWT, fetch it back, verify match
    }
    finally { Directory.Delete(dir, recursive: true); }
}

Step 6: Account tests (31 tests)

These test account route mapping, imports, exports — pure account logic:

Read existing AccountTests.cs, then add 31 methods. Example:

[Fact] // T:80
public void AccountMultipleServiceImports_SameSubject_ShouldSucceed()
{
    // Reference: golang/nats-server/server/accounts_test.go line 2535
    // Test that an account can have multiple service imports for the same subject
    // from different remote accounts
}

Step 7: Run tests

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~JetStreamErrorsTests|FullyQualifiedName~JetStreamVersioningTests|FullyQualifiedName~JetStreamBatchingTests|FullyQualifiedName~DirectoryStoreTests"

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ \
  --filter "FullyQualifiedName~AccountTests"

dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/

Step 8: Mark complete and commit

# Mark all P7-09 tests complete
sqlite3 porting.db "
UPDATE unit_tests SET status='complete'
WHERE module_id=8 AND status='not_started' AND go_file IN (
  'server/jetstream_errors_test.go',
  'server/jetstream_versioning_test.go',
  'server/jetstream_batching_test.go',
  'server/dirstore_test.go',
  'server/accounts_test.go'
);"

./reports/generate-report.sh
git add dotnet/tests/ porting.db reports/
git commit -m "feat(p7-09): port JetStream error/versioning/batching/dirstore/accounts tests (94 tests)"

Task 11: P7-10 — Mark Deferred, Integration Tests, Phase Close

This session has three parts: (A) mark 2,126 server-integration tests deferred, (B) replace integration test placeholder, (C) close DB + milestones.

Part A: Mark server-integration tests as deferred

All not_started tests that require a running NATS server are marked deferred:

sqlite3 porting.db "
UPDATE unit_tests SET status='deferred'
WHERE status='not_started'
AND go_file NOT IN (
  'server/accounts_test.go',
  'server/dirstore_test.go',
  'server/memstore_test.go',
  'server/store_test.go',
  'server/filestore_test.go',
  'server/jetstream_errors_test.go',
  'server/jetstream_versioning_test.go',
  'server/jetstream_batching_test.go',
  'server/opts_test.go'
);"

Verify:

sqlite3 porting.db "SELECT status, COUNT(*) FROM unit_tests GROUP BY status ORDER BY status;"

Expected after this update:

  • complete: ~1,256 (319 old + ~937 new from P7-02 through P7-09)
  • deferred: ~2,126
  • n_a: ~187 (181 old + 6 thw benchmarks)
  • stub: 0 (all filled)
  • not_started: 0
  • verified: ~114 (small modules)

Part B: Replace integration test placeholder

Replace dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/UnitTest1.cs with NatsServerBehaviorTests.cs.

These tests run against the Go NATS server (not the .NET server). They establish a behavioral baseline.

Prerequisites: Go server must be running: cd golang/nats-server && go run . -p 4222

Create dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/NatsServerBehaviorTests.cs:

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0

using NATS.Client.Core;
using Shouldly;

namespace ZB.MOM.NatsNet.Server.IntegrationTests;

/// <summary>
/// Behavioral baseline tests against the reference Go NATS server.
/// These tests require a running Go NATS server on localhost:4222.
/// Start with: cd golang/nats-server && go run . -p 4222
/// </summary>
[Trait("Category", "Integration")]
public class NatsServerBehaviorTests : IAsyncLifetime
{
    private NatsConnection? _nats;

    public async Task InitializeAsync()
    {
        _nats = new NatsConnection(new NatsOpts { Url = "nats://localhost:4222" });
        await _nats.ConnectAsync();
    }

    public async Task DisposeAsync()
    {
        if (_nats is not null)
            await _nats.DisposeAsync();
    }

    [Fact]
    public async Task BasicPubSub_ShouldDeliverMessage()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var received = new TaskCompletionSource<string>();

        await using var sub = await _nats!.SubscribeAsync<string>("test.hello", cancellationToken: cts.Token);
        _ = Task.Run(async () =>
        {
            await foreach (var msg in sub.Msgs.ReadAllAsync(cts.Token))
            {
                received.TrySetResult(msg.Data ?? "");
                break;
            }
        }, cts.Token);

        await _nats.PublishAsync("test.hello", "world");
        var result = await received.Task.WaitAsync(cts.Token);
        result.ShouldBe("world");
    }

    [Fact]
    public async Task WildcardSubscription_DotStar_ShouldMatch()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var received = new TaskCompletionSource<string>();

        await using var sub = await _nats!.SubscribeAsync<string>("foo.*", cancellationToken: cts.Token);
        _ = Task.Run(async () =>
        {
            await foreach (var msg in sub.Msgs.ReadAllAsync(cts.Token))
            {
                received.TrySetResult(msg.Subject);
                break;
            }
        }, cts.Token);

        await _nats.PublishAsync("foo.bar", "payload");
        var subject = await received.Task.WaitAsync(cts.Token);
        subject.ShouldBe("foo.bar");
    }

    [Fact]
    public async Task WildcardSubscription_GreaterThan_ShouldMatchMultiLevel()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var received = new TaskCompletionSource<string>();

        await using var sub = await _nats!.SubscribeAsync<string>("foo.>", cancellationToken: cts.Token);
        _ = Task.Run(async () =>
        {
            await foreach (var msg in sub.Msgs.ReadAllAsync(cts.Token))
            {
                received.TrySetResult(msg.Subject);
                break;
            }
        }, cts.Token);

        await _nats.PublishAsync("foo.bar.baz", "payload");
        var subject = await received.Task.WaitAsync(cts.Token);
        subject.ShouldBe("foo.bar.baz");
    }

    [Fact]
    public async Task QueueGroup_ShouldDeliverToOnlyOneSubscriber()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var count1 = 0;
        var count2 = 0;

        await using var sub1 = await _nats!.SubscribeAsync<string>("qg.test", queueGroup: "workers", cancellationToken: cts.Token);
        await using var sub2 = await _nats!.SubscribeAsync<string>("qg.test", queueGroup: "workers", cancellationToken: cts.Token);

        _ = Task.Run(async () =>
        {
            await foreach (var _ in sub1.Msgs.ReadAllAsync(cts.Token))
                Interlocked.Increment(ref count1);
        }, cts.Token);
        _ = Task.Run(async () =>
        {
            await foreach (var _ in sub2.Msgs.ReadAllAsync(cts.Token))
                Interlocked.Increment(ref count2);
        }, cts.Token);

        for (var i = 0; i < 10; i++)
            await _nats.PublishAsync("qg.test", $"msg{i}");

        await Task.Delay(500, cts.Token);
        (count1 + count2).ShouldBe(10);
        count1.ShouldBeGreaterThan(0);
        count2.ShouldBeGreaterThan(0);
    }

    [Fact]
    public async Task ConnectDisconnect_ShouldNotThrow()
    {
        var nats2 = new NatsConnection(new NatsOpts { Url = "nats://localhost:4222" });
        await Should.NotThrowAsync(async () =>
        {
            await nats2.ConnectAsync();
            await nats2.DisposeAsync();
        });
    }
}

Delete the placeholder:

rm dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/UnitTest1.cs

Run the integration tests (requires Go server running):

# In terminal 1: start Go server
cd golang/nats-server && go run . -p 4222

# In terminal 2: run integration tests
dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/

Part C: Final DB verification and milestone close

Mark server module verified (features and module)

After all unit tests are written and verified:

# Mark all complete tests in server module as verified
sqlite3 porting.db "
UPDATE unit_tests SET status='verified'
WHERE module_id=8 AND status='complete';"

# Mark all features in server module verified
dotnet run --project tools/NatsNet.PortTracker -- feature update 0 --status verified --all-in-module 8 --db porting.db

# Mark server module verified
dotnet run --project tools/NatsNet.PortTracker -- module update 8 --status verified --db porting.db

Run phase check

dotnet run --project tools/NatsNet.PortTracker -- phase check 7 --db porting.db

Generate final report

./reports/generate-report.sh
dotnet run --project tools/NatsNet.PortTracker -- report summary --db porting.db

Close Gitea issues #45#52

for issue in 45 46 47 48 49 50 51 52; do
  curl -s -X PATCH "https://gitea.dohertylan.com/api/v1/repos/dohertj2/natsnet/issues/${issue}" \
    -H "Content-Type: application/json" \
    -H "Authorization: token $GITEA_TOKEN" \
    -d '{"state":"closed"}'
done

Close milestones 7 and 8

for milestone in 7 8; do
  curl -s -X PATCH "https://gitea.dohertylan.com/api/v1/repos/dohertj2/natsnet/milestones/${milestone}" \
    -H "Content-Type: application/json" \
    -H "Authorization: token $GITEA_TOKEN" \
    -d '{"state":"closed"}'
done

Final commit

git add porting.db reports/ dotnet/tests/
git commit -m "feat(p7-10): mark deferred tests, add integration tests, close Phase 7

- 2126 server-integration tests marked deferred
- NatsServerBehaviorTests.cs replaces UnitTest1.cs placeholder
- Server module and all features marked verified
- Gitea issues #45-#52 and milestones 7 & 8 closed"

Completion Checklist

  • Schema: unit_tests.status includes 'deferred'
  • 11 small modules: status verified
  • All 224 stubs: status complete or deferred (none remain as stub)
  • ~401 unit tests: status complete (new test methods written)
  • ~2,126 server-integration tests: status deferred
  • 6 thw benchmarks: status n_a
  • dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ passes
  • dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ passes
  • dotnet run --project tools/NatsNet.PortTracker -- phase check 7 --db porting.db passes
  • Gitea issues #45#52 closed
  • Gitea milestones 7 and 8 closed