@@ -298,6 +298,16 @@ fn write_pricing_cache(base: &Path, timestamp: u64) {
298298 }
299299}
300300
301+ fn write_fake_credentials ( base : & Path ) {
302+ let creds_dir = base. join ( ".config/tokscale" ) ;
303+ fs:: create_dir_all ( & creds_dir) . unwrap ( ) ;
304+ fs:: write (
305+ creds_dir. join ( "credentials.json" ) ,
306+ r#"{"token":"fake","username":"testuser","createdAt":"2024-01-01T00:00:00Z"}"# ,
307+ )
308+ . unwrap ( ) ;
309+ }
310+
301311// ── Existing tests ─────────────────────────────────────────────────────────
302312
303313#[ test]
@@ -900,6 +910,12 @@ fn test_models_json_offline_without_pricing_cache_still_succeeds() {
900910 assert_eq ! ( json[ "totalOutput" ] . as_i64( ) . unwrap( ) , 1000 ) ;
901911 assert_eq ! ( json[ "totalMessages" ] . as_i64( ) . unwrap( ) , 3 ) ;
902912 assert_eq ! ( json[ "entries" ] . as_array( ) . unwrap( ) . len( ) , 2 ) ;
913+ // Without pricing, embedded source costs are preserved (0.05 + 0.03 + 0.02)
914+ let total_cost = json[ "totalCost" ] . as_f64 ( ) . unwrap ( ) ;
915+ assert ! (
916+ ( total_cost - 0.10 ) . abs( ) < 1e-9 ,
917+ "unexpected totalCost without pricing: {total_cost}"
918+ ) ;
903919}
904920
905921#[ test]
@@ -920,6 +936,12 @@ fn test_monthly_json_offline_without_pricing_cache_still_succeeds() {
920936 assert_eq ! ( entries. len( ) , 2 ) ;
921937 assert_eq ! ( entries[ 0 ] [ "month" ] . as_str( ) . unwrap( ) , "2024-06" ) ;
922938 assert_eq ! ( entries[ 1 ] [ "month" ] . as_str( ) . unwrap( ) , "2025-01" ) ;
939+ // Without pricing, embedded source costs are preserved
940+ let total_cost = json[ "totalCost" ] . as_f64 ( ) . unwrap ( ) ;
941+ assert ! (
942+ ( total_cost - 0.10 ) . abs( ) < 1e-9 ,
943+ "unexpected totalCost without pricing: {total_cost}"
944+ ) ;
923945}
924946
925947#[ test]
@@ -939,6 +961,12 @@ fn test_graph_offline_without_pricing_cache_still_succeeds() {
939961 assert_eq ! ( json[ "summary" ] [ "totalTokens" ] . as_i64( ) . unwrap( ) , 3950 ) ;
940962 assert_eq ! ( json[ "summary" ] [ "activeDays" ] . as_i64( ) . unwrap( ) , 2 ) ;
941963 assert_eq ! ( json[ "contributions" ] . as_array( ) . unwrap( ) . len( ) , 2 ) ;
964+ // Without pricing, embedded source costs are preserved
965+ let total_cost = json[ "summary" ] [ "totalCost" ] . as_f64 ( ) . unwrap ( ) ;
966+ assert ! (
967+ ( total_cost - 0.10 ) . abs( ) < 1e-9 ,
968+ "unexpected totalCost without pricing: {total_cost}"
969+ ) ;
942970}
943971
944972#[ test]
@@ -964,6 +992,52 @@ fn test_models_json_offline_uses_stale_pricing_cache_when_available() {
964992 ) ;
965993}
966994
995+ #[ test]
996+ fn test_monthly_json_offline_uses_stale_pricing_cache_when_available ( ) {
997+ let tmp = create_temp_fixture_dir_without_pricing_cache ( ) ;
998+ write_pricing_cache ( tmp. path ( ) , 1 ) ;
999+
1000+ let output = offline_cmd_with_home ( tmp. path ( ) )
1001+ . args ( [ "monthly" , "--json" , "--opencode" , "--no-spinner" ] )
1002+ . output ( )
1003+ . unwrap ( ) ;
1004+ assert ! (
1005+ output. status. success( ) ,
1006+ "stderr: {}" ,
1007+ String :: from_utf8_lossy( & output. stderr)
1008+ ) ;
1009+
1010+ let json: serde_json:: Value = serde_json:: from_slice ( & output. stdout ) . unwrap ( ) ;
1011+ let total_cost = json[ "totalCost" ] . as_f64 ( ) . unwrap ( ) ;
1012+ assert ! (
1013+ ( total_cost - 0.0209 ) . abs( ) < 1e-9 ,
1014+ "unexpected totalCost: {total_cost}"
1015+ ) ;
1016+ }
1017+
1018+ #[ test]
1019+ fn test_graph_offline_uses_stale_pricing_cache_when_available ( ) {
1020+ let tmp = create_temp_fixture_dir_without_pricing_cache ( ) ;
1021+ write_pricing_cache ( tmp. path ( ) , 1 ) ;
1022+
1023+ let output = offline_cmd_with_home ( tmp. path ( ) )
1024+ . args ( [ "graph" , "--opencode" , "--no-spinner" ] )
1025+ . output ( )
1026+ . unwrap ( ) ;
1027+ assert ! (
1028+ output. status. success( ) ,
1029+ "stderr: {}" ,
1030+ String :: from_utf8_lossy( & output. stderr)
1031+ ) ;
1032+
1033+ let json: serde_json:: Value = serde_json:: from_slice ( & output. stdout ) . unwrap ( ) ;
1034+ let total_cost = json[ "summary" ] [ "totalCost" ] . as_f64 ( ) . unwrap ( ) ;
1035+ assert ! (
1036+ ( total_cost - 0.0209 ) . abs( ) < 1e-9 ,
1037+ "unexpected totalCost: {total_cost}"
1038+ ) ;
1039+ }
1040+
9671041#[ test]
9681042fn test_models_json_total_consistency ( ) {
9691043 let tmp = create_temp_fixture_dir ( ) ;
@@ -1582,3 +1656,29 @@ fn test_root_with_group_by() {
15821656 let json: serde_json:: Value = serde_json:: from_slice ( & output. stdout ) . unwrap ( ) ;
15831657 assert_eq ! ( json[ "groupBy" ] . as_str( ) . unwrap( ) , "model" ) ;
15841658}
1659+
1660+ #[ test]
1661+ fn test_submit_offline_without_pricing_cache_fails ( ) {
1662+ let tmp = create_temp_fixture_dir_without_pricing_cache ( ) ;
1663+ write_fake_credentials ( tmp. path ( ) ) ;
1664+
1665+ let output = offline_cmd_with_home ( tmp. path ( ) )
1666+ . args ( [ "submit" , "--opencode" , "--dry-run" ] )
1667+ . output ( )
1668+ . unwrap ( ) ;
1669+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
1670+ assert ! (
1671+ !output. status. success( ) ,
1672+ "submit should fail when pricing is unavailable; stdout: {}" ,
1673+ String :: from_utf8_lossy( & output. stdout)
1674+ ) ;
1675+ // Verify failure is from pricing fetch, not from auth or argument errors
1676+ assert ! (
1677+ !stderr. contains( "Not logged in" ) ,
1678+ "submit failed due to auth, not pricing: {stderr}"
1679+ ) ;
1680+ assert ! (
1681+ stderr. contains( "error" ) || stderr. contains( "Error" ) ,
1682+ "stderr should contain a pricing/network error: {stderr}"
1683+ ) ;
1684+ }
0 commit comments