Agent Guidelines for tests
June 4, 2026 · View on GitHub
Test infrastructure for VNote using the QtTest framework. All tests live under tests/ and are built via CMake's add_qt_test() helper. Tests exercise services, models, controllers, and utilities against the vxcore C backend in isolated test mode.
Test Structure
tests/
├── CMakeLists.txt # Root test config with add_qt_test() helper
├── helpers/
│ ├── CMakeLists.txt
│ ├── test_helper.h # Common includes
│ └── temp_dir_fixture.h # QTemporaryDir wrapper (+ copyFrom() for fixtures)
├── data/ # On-disk test fixtures (see "Test Data Fixtures" below)
│ └── vnote3_notebooks/
│ └── database_notebook/ # Real VNote3 notebook with 17 files + subfolder
├── core/
│ ├── CMakeLists.txt
│ ├── test_error.cpp
│ ├── test_exception.cpp
│ ├── test_configservice.cpp
│ ├── test_notebookservice.cpp
│ ├── test_bufferservice.cpp # BufferCoreService tests
│ ├── test_buffer.cpp # Buffer2 + BufferService integration tests (29 cases)
│ └── test_vnote3migrationservice.cpp # VNote3 migration tests (46 cases)
└── utils/
├── CMakeLists.txt
├── test_pathutils.cpp
└── test_htmlutils.cpp
CRITICAL: Test Mode for vxcore
Always enable test mode BEFORE creating vxcore context in tests:
void TestMyService::initTestCase() {
// CRITICAL: Must call BEFORE vxcore_context_create()
// Prevents tests from corrupting real user data
vxcore_set_test_mode(1);
m_ctx = vxcore_context_create();
// ...
}
Why this matters:
- Without test mode, vxcore uses real
AppData/Localpaths - Tests will corrupt actual user configuration
- Test mode redirects to isolated temp directories
Writing Service Tests
// tests/core/test_myservice.cpp
#include <QtTest>
#include <vxcore/vxcore.h>
#include <core/myservice.h>
namespace tests {
class TestMyService : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testBasicOperation();
private:
vxcore_context *m_ctx = nullptr;
};
void TestMyService::initTestCase() {
vxcore_set_test_mode(1); // CRITICAL!
m_ctx = vxcore_context_create();
}
void TestMyService::cleanupTestCase() {
vxcore_context_destroy(m_ctx);
m_ctx = nullptr;
}
void TestMyService::testBasicOperation() {
vnotex::MyService service(m_ctx);
QVERIFY(!service.doSomething().isEmpty());
}
} // namespace tests
QTEST_GUILESS_MAIN(tests::TestMyService)
#include "test_myservice.moc"
Enable Tests
Uncomment in root CMakeLists.txt:
add_subdirectory(tests)
Build Tests
cmake --build build --config Release --target test_error test_exception test_pathutils test_htmlutils
Test Libraries
| Library | Purpose | Links |
|---|---|---|
core_services | Service layer (ConfigCoreService, NotebookCoreService, etc.) | Qt6::Core, Qt6::Gui, vxcore |
core_configs | Config classes (ConfigMgr2, MainConfig, SessionConfig, etc.) | core_services, VTextEdit |
Run Tests
Windows (Qt DLLs must be in PATH):
$env:PATH = "C:/Qt/6.9.3/msvc2022_64/bin;" + $env:PATH
./build/tests/core/test_error.exe
./build/tests/core/test_configservice.exe
./build/tests/core/test_notebookservice.exe
Using CTest (requires Qt in system PATH):
ctest --test-dir build # Run all tests
ctest --test-dir build -R test_error # Run single test (pattern match)
ctest --test-dir build --output-on-failure # Show output on failure
VTextEdit.dll runtime copy
Tests that link core_services (transitively or directly) load VTextEdit.dll at runtime. The build copies the DLL next to each subdirectory's test exes via a POST_BUILD step anchored on one test target per subdir (the canonical reference is tests/utils/CMakeLists.txt:78-83):
# Copy VTextEdit DLL next to test executable so CTest can find it at runtime.
add_custom_command(TARGET test_clipboard_image POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:VTextEdit>
$<TARGET_FILE_DIR:test_clipboard_image>
)
When adding tests to a NEW subdirectory under tests/, that subdir's CMakeLists.txt MUST include this POST_BUILD block anchored on any one test target. Without it, ctest reports the test as failed with exit code 0xc0000135 (Windows "DLL not found") and the test binary never reaches its main(). One copy per subdir is enough; copy_if_different makes incremental rebuilds free.
Adding New Tests
Use the add_qt_test() helper function in CMakeLists.txt:
# tests/module/CMakeLists.txt
add_qt_test(test_myclass
SOURCES
test_myclass.cpp
${CMAKE_SOURCE_DIR}/src/module/myclass.cpp
LINKS
Qt6::Gui # Optional: extra Qt modules
GUILESS # Optional: use QCoreApplication instead of QApplication
)
Qt Test Macros
| Macro | Purpose |
|---|---|
QTEST_MAIN(Class) | Creates main(), runs tests with QApplication |
QTEST_GUILESS_MAIN(Class) | Headless test runner (preferred) |
QVERIFY(condition) | Assert condition is true |
QCOMPARE(actual, expected) | Assert equality |
QFETCH(type, name) | Fetch data-driven test value |
QTest::addColumn<T>("name") | Declare data column |
QTest::newRow("name") | Add data row |
QSKIP("reason") | Skip test |
QTest::ignoreMessage(type, msg) | Suppress expected qDebug/qWarning/qCritical |
Test Data Fixtures
Tests that need realistic directory structures (e.g., legacy notebook migration) use on-disk fixtures under tests/data/ instead of hardcoding JSON and file creation in C++.
Pattern
- Create fixture directory under
tests/data/with real files on disk - Locate at runtime using
QFINDTESTDATA(works out of the box with Qt6+CMake) - Copy to temp dir using
TempDirFixture::copyFrom()for test isolation
Example
// In your test class
QString findFixture(const QString &p_relPath) {
// Path is relative to the test source file
QString path = QFINDTESTDATA(p_relPath);
QVERIFY2_RETURN(!path.isEmpty(),
qPrintable(QStringLiteral("Fixture not found: %1").arg(p_relPath)),
QString());
return path;
}
void TestMyService::testWithFixture() {
// Locate fixture (relative to this .cpp file's directory)
QString fixturePath = findFixture(
QStringLiteral("../data/vnote3_notebooks/database_notebook"));
QVERIFY2(!fixturePath.isEmpty(), "Fixture not found");
// Copy to isolated temp dir
TempDirFixture workDir;
QString sourceDir = workDir.copyFrom(fixturePath, QStringLiteral("source"));
QVERIFY2(!sourceDir.isEmpty(), "Failed to copy fixture");
// Test against the copy
// ...
}
Adding New Fixtures
- Create directory under
tests/data/with descriptive name - Add all files the test needs (stubs are fine — content doesn't have to be real)
- For Chinese/Unicode filenames, the files must exist on disk with those names
- No CMake changes needed —
QFINDTESTDATAresolves paths automatically viaQT_TESTCASE_SOURCEDIR
Existing Fixtures
| Fixture | Path | Description |
|---|---|---|
| database_notebook | tests/data/vnote3_notebooks/database_notebook/ | VNote3 notebook with 17 files (Chinese + ASCII), 1 subfolder, 3 attachment dirs |
Real Keychain Usage
All new or modified tests that instantiate the real SyncCredentialsStore (not a MockCredentialsStore / in-memory fake) MUST register a tests::KeychainGuard to track and clean up written PAT entries.
Why
Without deterministic cleanup, tests leak PAT entries into the host OS keychain. Fixed-ID tests overwrite themselves harmlessly, but UUID-based writes accumulate forever. QtKeychain has no enumerate API (see src/core/services/synccredentialsstore.cpp:71), so cleanup cannot be done by sweeping the keychain at startup; it must be driven by tracking each write.
How
#include "../helpers/keychain_guard.h"
// Class-level member:
tests::KeychainGuard *m_keychainGuard = nullptr;
// In initTestCase(), after registering SyncCredentialsStore:
auto *credStore = m_services.get<vnotex::SyncCredentialsStore>();
m_keychainGuard = new tests::KeychainGuard(credStore, this);
// In cleanupTestCase(), BEFORE destroying the vxcore context:
if (m_keychainGuard) {
m_keychainGuard->cleanup();
delete m_keychainGuard;
m_keychainGuard = nullptr;
}
// For indirect writes (e.g. via SyncService::enableSyncForNotebook),
// also call track() defensively in case the credentialsStored signal races:
m_keychainGuard->track(notebookId);
Ordering
cleanup() MUST run before the vxcore context is destroyed. The guard talks to SyncCredentialsStore, which depends on ServiceLocator and the live vxcore context; tearing the context down first leaves the guard with dangling references.
Cross-reference
For the prod-side cleanup contract (5 sites in SyncService), see ../src/core/services/AGENTS.md § Credential Cleanup Invariants.
Mandatory KeychainGuard rollout (post-fix-failing-tests plan)
All sync tests that instantiate SyncCredentialsStore (whether through ServiceLocator or by local construction) MUST register a tests::KeychainGuard. This is non-negotiable: missing guards cause cumulative notebook_sync_pat_<uuid> accumulation in the Windows Credential Manager, eventually tripping Win32 error 8 ("Not enough storage") which cascades every sync test into failure on subsequent runs.
As of 2026-06-05, the following tests are compliant:
test_synccredentialsstore(canonical reference)test_sync_signal_auto_baseline,test_sync_signal_baselinetest_sync_hooks,test_sync_ops,test_sync_auto_route,test_sync_close_blocktest_bootstrap_and_persisttest_syncservice,test_syncservice_lifecycle
(10 tests total.) New sync tests MUST follow the template in the "Real Keychain Usage → How" section above. Tests that do NOT touch the keychain (test_eventbridge_sync, test_notebookcoreservice_sync) are exempt.
Operator note (Windows): the keychain entry surfaces in cmdkey /list as LegacyGeneric:target=notebook_sync_pat_<uuid> (CRED_TYPE_GENERIC). KeychainGuard::cleanup() delegates to SyncCredentialsStore::deleteCredentials(), which handles the full target name internally; operators do NOT need to manage the LegacyGeneric:target= prefix manually when invoking the guard.
Current Test Coverage
| Test | Class | Test Cases |
|---|---|---|
| test_error | Error, ErrorCode | 23 |
| test_exception | Exception | 20 |
| test_pathutils | PathUtils | 68 |
| test_htmlutils | HtmlUtils | 31 |
| test_fileutils2 | FileUtils2 | 15 |
| test_configservice | ConfigCoreService | 10 |
| test_notebookservice | NotebookCoreService | 33 |
| test_bufferservice | BufferCoreService | - |
| test_buffer | Buffer2 + BufferService | 29 |
| test_vnote3migrationservice | VNote3MigrationService | 46 |
| test_searchservice | SearchCoreService | - |
| test_servicelocator | ServiceLocator | - |
| test_configmgr2 | ConfigMgr2 | 7 |
| test_hookmanager | HookManager | 24 |
| test_hookintegration | Hook integration | 10 |
| Total | 267+ |
Debugging Tips
Windows exit code 0xC0000409 is NOT a stack overrun
0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) is misleading. On Windows, Qt's qFatal() calls __fastfail(FAST_FAIL_FATAL_APP_EXIT), which the OS reports as 0xC0000409. The actual cause is almost always a qFatal() deep inside Qt, e.g., QFontDatabase: Must construct a QGuiApplication before accessing QFontDatabase raised when a QTEST_GUILESS_MAIN test transitively touches font-database code.
When a test exits with this code, re-run it with file-based output capture to surface the real Qt diagnostic:
$env:PATH = "C:/Qt/6.9.3/msvc2022_64/bin;" + $env:PATH
& "build/.../tests/<category>/test_name.exe" -o "output.txt,txt"
Get-Content output.txt | Select-String "QFATAL|FAIL"
The QFATAL : ... message line names the real cause. Do NOT chase the test for recursion, oversized stack allocations, or fixture loops; that is a false trail. Fix the underlying Qt precondition (switch to QTEST_MAIN, guard the GUI-dependent code path, or short-circuit before the offending Qt subsystem is touched).
Related Modules
../src/core/AGENTS.md— Services and hooks being tested../AGENTS.md— Code style, architecture overview