Steps to Reproduce Issue
Create a package with a test module placed in the tests/ directory (so it is compiled only in test mode) whose name matches an OTW-shaped struct, and construct that OTW manually:
// tests/my_module_tests.move
module my_pkg::my_module_tests;
public struct MY_MODULE_TESTS has drop {}
fun new_then_drop_otw() {
let _otw = MY_MODULE_TESTS {}; // manual OTW construction
}
#[test]
fun some_test() {
new_then_drop_otw();
}
- Run
sui move test --build-env testnet. Confirm failure.
$ sui move test --build-env testnet
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY Sui
BUILDING my_pkg
error[Sui E02005]: invalid one-time witness usage
┌─ ./sources/my_module_tests.move:7:16
│
7 │ let _otw = MY_MODULE_TESTS {}; // manual OTW construction
│ ^^^^^^^^^^^^^^^^^^ Invalid one-time witness construction. One-time witness types cannot be created manually, but are passed as an argument 'init'
│
= One-time witness types are structs with the following requirements: their name is the upper-case version of the module's name, they have no fields (or a single boolean field), they have no type parameters, and they have only the 'drop' ability.
- Add
#[test_only] to the module (#[test_only] module my_pkg::my_module_tests;) and run sui move test --build-env testnet again.
$ sui move test --build-env testnet
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY Sui
BUILDING my_pkg
Running Move unit tests
[ PASS ] my_pkg::my_module_tests::some_test
Test result: OK. Total tests: 1; passed: 1; failed: 0
- Add
#[test_only] to the new_then_drop_otw function (#[test_only] fun new_then_drop_otw()) and run sui move test --build-env testnet again.
$ sui move test --build-env testnet
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY Sui
BUILDING my_pkg
Running Move unit tests
[ PASS ] my_pkg::my_module_tests::some_test
Test result: OK. Total tests: 1; passed: 1; failed: 0
Expected Result
All 3 runs should compile. A module in tests/ is compiled only in test mode, so #[test_only] is semantically redundant there, and the existing OTW test-code carve-out should apply either way.
Expected the OTW manual construction to be allowed in step 1 (no #[test_only]), just as it is in steps 2 and 3.
Actual Result
Step 1 (no #[test_only]) fails to compile:
Invalid one-time witness construction. One-time witness types cannot be created
manually, but are passed as an argument 'init'
Steps 2 and 3 (with #[test_only] on module or function level) compiles fine. The carve-out is gated on the #[test]/#[test_only] attribute of the enclosing module/function (context.in_test in external-crates/move/crates/move-compiler/src/sui_mode/typing.rs, set only from attributes.is_test_or_test_only()), not on whether the code is compiled in test mode / lives in tests/. The same gate also governs the "cannot call init directly" check.
Suggested fix: treat all modules compiled in test-only mode (everything under tests/) as in_test = true, so the attribute is genuinely optional there.
System Information
- OS: macOS 26.5.1 (build 25F80)
- Compiler: sui 1.72.5-5445323ef25aq
Steps to Reproduce Issue
Create a package with a test module placed in the
tests/directory (so it is compiled only in test mode) whose name matches an OTW-shaped struct, and construct that OTW manually:sui move test --build-env testnet. Confirm failure.#[test_only]to the module (#[test_only] module my_pkg::my_module_tests;) and runsui move test --build-env testnetagain.#[test_only]to thenew_then_drop_otwfunction (#[test_only] fun new_then_drop_otw()) and runsui move test --build-env testnetagain.Expected Result
All 3 runs should compile. A module in
tests/is compiled only in test mode, so#[test_only]is semantically redundant there, and the existing OTW test-code carve-out should apply either way.Expected the OTW manual construction to be allowed in step 1 (no
#[test_only]), just as it is in steps 2 and 3.Actual Result
Step 1 (no
#[test_only]) fails to compile:Steps 2 and 3 (with
#[test_only]on module or function level) compiles fine. The carve-out is gated on the#[test]/#[test_only]attribute of the enclosing module/function (context.in_testin external-crates/move/crates/move-compiler/src/sui_mode/typing.rs, set only fromattributes.is_test_or_test_only()), not on whether the code is compiled in test mode / lives intests/. The same gate also governs the "cannot callinitdirectly" check.Suggested fix: treat all modules compiled in test-only mode (everything under
tests/) asin_test = true, so the attribute is genuinely optional there.System Information