diff --git a/android/app/build.gradle b/android/app/build.gradle
index c349207777..4e5bdb4a44 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -170,12 +170,12 @@ android {
dimension "environment"
applicationId "me.tinykitten.trainlcd.dev"
versionNameSuffix "-dev"
- versionCode 100000833
+ versionCode 100000835
versionName "10.17.0"
}
prod {
dimension "environment"
- versionCode 100000833
+ versionCode 100000835
versionName "10.17.0"
}
// ストア配信する dev (Canary) と applicationId を分けた、ローカル検証専用のフレーバー。
@@ -186,7 +186,7 @@ android {
dimension "environment"
applicationId "me.tinykitten.trainlcd.local"
versionNameSuffix "-local"
- versionCode 100000833
+ versionCode 100000835
versionName "10.17.0"
}
}
diff --git a/android/wearable/build.gradle.kts b/android/wearable/build.gradle.kts
index 03baf4fda2..4ce670ec93 100644
--- a/android/wearable/build.gradle.kts
+++ b/android/wearable/build.gradle.kts
@@ -85,12 +85,12 @@ android {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
- versionCode = 100000834
+ versionCode = 100000836
versionName = "10.17.0"
}
create("prod") {
dimension = "environment"
- versionCode = 100000834
+ versionCode = 100000836
versionName = "10.17.0"
}
// :app の local フレーバーと対になるローカル検証専用フレーバー。Wear の Data Layer は
@@ -100,7 +100,7 @@ android {
dimension = "environment"
applicationIdSuffix = ".local"
versionNameSuffix = "-local"
- versionCode = 100000834
+ versionCode = 100000836
versionName = "10.17.0"
}
}
diff --git a/app.config.ts b/app.config.ts
index 95bfa7e484..0886eb3a08 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -64,7 +64,7 @@ export default {
userInterfaceStyle: 'automatic',
// Expo SDK 57 の各モジュール(expo / expo-modules-core ほか)は podspec で iOS 16.4 以上を要求する
deploymentTarget: '16.4',
- buildNumber: '2946',
+ buildNumber: '2947',
scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD',
bundleIdentifier: IS_DEV
? 'me.tinykitten.trainlcd.dev'
@@ -78,7 +78,7 @@ export default {
? 'me.tinykitten.trainlcd.dev'
: 'me.tinykitten.trainlcd',
permissions: [],
- versionCode: 100000833,
+ versionCode: 100000835,
},
owner: 'trainlcd',
experiments: {
diff --git a/docs/location-simulation.md b/docs/location-simulation.md
index 9ff2e72c3c..66c8034cf7 100644
--- a/docs/location-simulation.md
+++ b/docs/location-simulation.md
@@ -87,6 +87,12 @@ npm run gpx:generate -- \
ごとの速度を実ダイヤで較正した到着時間推定から出る。アプリのオートモード
(`useSimulationMode`) も同じ見込みで走るので、GPX・オートモード・ETA の時間がそろう。
+オートモードがこの見込みで走るのは、Worker の `/config/remote` が
+`auto_mode_estimated_enabled: true` を配信しているときだけ。`false`・未配信・取得失敗の
+ときは、`model` を渡さない従来の値 (`Legacy`) で区間の最高速度を出して走る。値は Worker の
+KV `config:remote` に入れる。`true` でも StationAPI が `model` を知らずにエラーを返したときは、
+`Legacy` に戻して走る。
+
- 停車駅から次の停車駅までの時間は「次の停車駅の到着 − この駅の出発」。推定では停まるが
GPX では通過する駅 (平日運転の駅を休日に走る、`--skip` など) は、その駅の停車時間を
走行から引く。
diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj
index f23b05ca47..16ea1cc7d2 100644
--- a/ios/TrainLCD.xcodeproj/project.pbxproj
+++ b/ios/TrainLCD.xcodeproj/project.pbxproj
@@ -2562,7 +2562,7 @@
CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = E6R2G33Z36;
INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist;
@@ -2602,7 +2602,7 @@
CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEVELOPMENT_TEAM = E6R2G33Z36;
INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TrainLCD;
@@ -2661,7 +2661,7 @@
CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -2768,7 +2768,7 @@
CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -2849,7 +2849,7 @@
CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = E6R2G33Z36;
INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist;
@@ -2889,7 +2889,7 @@
CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist;
@@ -3101,7 +3101,7 @@
CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3153,7 +3153,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3204,7 +3204,7 @@
CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3262,7 +3262,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3313,7 +3313,7 @@
CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3370,7 +3370,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3419,7 +3419,7 @@
CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3471,7 +3471,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -3690,7 +3690,7 @@
CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -3746,7 +3746,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -3796,7 +3796,7 @@
CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -3854,7 +3854,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 2946;
+ CURRENT_PROJECT_VERSION = 2947;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = E6R2G33Z36;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
diff --git a/src/hooks/useAutoModeEstimatedEnabled.ts b/src/hooks/useAutoModeEstimatedEnabled.ts
new file mode 100644
index 0000000000..a9d5a7c6b1
--- /dev/null
+++ b/src/hooks/useAutoModeEstimatedEnabled.ts
@@ -0,0 +1,11 @@
+import { useSyncExternalStore } from 'react';
+import {
+ isAutoModeEstimatedEnabled,
+ subscribeRemoteConfig,
+} from '~/lib/remoteConfig';
+
+// オートモードの走らせ方(auto_mode_estimated_enabled)のリアクティブ版。
+// setupRemoteConfig は起動時に非同期で完了するため、同期読みだけではコールドスタート時の
+// キャッシュ更新に追従できない。useAIAgentFeatureEnabled と同じくキャッシュ更新を購読する。
+export const useAutoModeEstimatedEnabled = (): boolean =>
+ useSyncExternalStore(subscribeRemoteConfig, isAutoModeEstimatedEnabled);
diff --git a/src/hooks/useSimulationMode.test.tsx b/src/hooks/useSimulationMode.test.tsx
index c56a66c369..56c78c9ba7 100644
--- a/src/hooks/useSimulationMode.test.tsx
+++ b/src/hooks/useSimulationMode.test.tsx
@@ -9,6 +9,7 @@ import {
StopCondition,
} from '~/@types/graphql';
import { YAMANOTE_LINE_ID } from '~/constants';
+import { useAutoModeEstimatedEnabled } from '~/hooks/useAutoModeEstimatedEnabled';
import * as useCurrentTrainTypeModule from '~/hooks/useCurrentTrainType';
import { useGraphQLQuery } from '~/hooks/useGraphQLQuery';
import { useLoopLine } from '~/hooks/useLoopLine';
@@ -61,6 +62,12 @@ jest.mock('~/store/atoms/location', () => ({
locationAtom: { toString: () => 'locationAtom' },
}));
+// オートモードの走らせ方のフラグ(auto_mode_estimated_enabled)。各テストの既定は
+// 到着時間推定の見込みで走る側(true)で、従来の値で走る側は個別に false にする
+jest.mock('~/hooks/useAutoModeEstimatedEnabled', () => ({
+ useAutoModeEstimatedEnabled: jest.fn(() => true),
+}));
+
jest.mock('~/hooks/useLoopLine', () => ({
useLoopLine: jest.fn(() => ({
isLoopLine: false,
@@ -258,6 +265,7 @@ describe('useSimulationMode', () => {
// 各テストは非ループ線を前提とする。ループ線テストで上書きした実装が
// 後続テストへ漏れないよう毎回明示的にリセットする。
(useLoopLine as jest.Mock).mockReturnValue({ isLoopLine: false });
+ (useAutoModeEstimatedEnabled as jest.Mock).mockReturnValue(true);
jest
.spyOn(useCurrentTrainTypeModule, 'useCurrentTrainType')
@@ -1805,6 +1813,72 @@ describe('useSimulationMode', () => {
);
});
+ it('auto_mode_estimated_enabled が false なら、model: Estimated を引かずに従来の値で走る', () => {
+ (useAutoModeEstimatedEnabled as jest.Mock).mockReturnValue(false);
+ const stations = timedStations();
+ setupAtomMocks(
+ { station: stations[0], stations, selectedDirection: 'INBOUND' },
+ { autoModeEnabled: true }
+ );
+ // 従来の問い合わせは見込みのフィールドを選ばないので、区間の最高速度だけが返る
+ mockTrainRoute(stations, { accel: 0.7, decel: 0.9 });
+ const generateSpy = jest.spyOn(
+ trainSpeedModule,
+ 'generateTrainSpeedProfile'
+ );
+
+ renderHook(() => useSimulationMode(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(useGraphQLQuery).toHaveBeenCalledWith(
+ GET_ESTIMATED_TRAIN_ROUTE,
+ expect.objectContaining({ skip: true })
+ );
+ expect(useGraphQLQuery).toHaveBeenCalledWith(
+ GET_TRAIN_ROUTE,
+ expect.objectContaining({ skip: false })
+ );
+ // 見込みではなく区間の最高速度で速度プロファイルを作る
+ expect(generateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ maxSpeed: 30 })
+ );
+ });
+
+ it('auto_mode_estimated_enabled が false なら、乗換経路も従来の問い合わせで引く', () => {
+ (useAutoModeEstimatedEnabled as jest.Mock).mockReturnValue(false);
+ const withTrainType = (station: Station, groupId: number): Station => ({
+ ...station,
+ trainType: { groupId } as Station['trainType'],
+ });
+ const stations = [
+ withTrainType(mockStation(9930138, 9930138, 35.76, 139.63, 99301), 7),
+ withTrainType(mockStation(9930128, 1130208, 35.69, 139.7, 99301), 7),
+ withTrainType(mockStation(1132104, 1130208, 35.69, 139.7, 11321), 170),
+ withTrainType(
+ mockStation(1132103, 1130205, 35.658, 139.701, 11321),
+ 170
+ ),
+ ];
+ setupAtomMocks(
+ { station: stations[0], stations, selectedDirection: 'INBOUND' },
+ { autoModeEnabled: true }
+ );
+
+ renderHook(() => useSimulationMode(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(useGraphQLQuery).toHaveBeenCalledWith(
+ GET_ESTIMATED_CONNECTED_TRAIN_ROUTE,
+ expect.objectContaining({ skip: true })
+ );
+ expect(useGraphQLQuery).toHaveBeenCalledWith(
+ GET_CONNECTED_TRAIN_ROUTE,
+ expect.objectContaining({ skip: false })
+ );
+ });
+
it('model: Estimated の問い合わせがエラーなら、従来の問い合わせの値で走る', () => {
const stations = timedStations();
setupAtomMocks(
diff --git a/src/hooks/useSimulationMode.ts b/src/hooks/useSimulationMode.ts
index 9f0866845a..c7a2d776ff 100644
--- a/src/hooks/useSimulationMode.ts
+++ b/src/hooks/useSimulationMode.ts
@@ -34,6 +34,7 @@ import {
} from '../store/atoms/station';
import dropEitherJunctionStation from '../utils/dropJunctionStation';
import getIsPass from '../utils/isPass';
+import { useAutoModeEstimatedEnabled } from './useAutoModeEstimatedEnabled';
import { useCurrentTrainType } from './useCurrentTrainType';
import { useGraphQLQuery } from './useGraphQLQuery';
import { useLoopLine } from './useLoopLine';
@@ -180,9 +181,10 @@ export const useSimulationMode = (): void => {
toStationId != null &&
fromStationId !== toStationId;
- // 区間の値は到着時間推定のモデル(model: Estimated)で取り、ETA と同じ時間で走らせる。
- // model を知らない StationAPI ではクエリ全体がエラーになるので、そのときだけ
- // 従来の問い合わせ(Legacy)に戻す
+ // auto_mode_estimated_enabled が true なら、区間の値を到着時間推定のモデル
+ // (model: Estimated)で取り、ETA と同じ時間で走らせる。model を知らない StationAPI では
+ // クエリ全体がエラーになるので、そのときだけ従来の問い合わせ(Legacy)に戻す。
+ // false・未配信なら最初から Legacy で引く
const singleVariables = {
fromStationId: fromStationId ?? 0,
toStationId: toStationId ?? 0,
@@ -195,42 +197,52 @@ export const useSimulationMode = (): void => {
};
const skipSingle = !canFetchTrainRoute || !!routeLegs;
const skipConnected = !canFetchTrainRoute || !routeLegs;
+ const estimatedEnabled = useAutoModeEstimatedEnabled();
const { data: estimatedSingleData, error: estimatedSingleError } =
useGraphQLQuery(
GET_ESTIMATED_TRAIN_ROUTE,
- { variables: singleVariables, skip: skipSingle }
+ { variables: singleVariables, skip: skipSingle || !estimatedEnabled }
);
const { data: legacySingleData, error: legacySingleError } = useGraphQLQuery<
TrainRouteData,
GetTrainRouteQueryVariables
>(GET_TRAIN_ROUTE, {
variables: singleVariables,
- skip: skipSingle || !estimatedSingleError,
+ skip: skipSingle || (estimatedEnabled && !estimatedSingleError),
});
const { data: estimatedConnectedData, error: estimatedConnectedError } =
useGraphQLQuery(
GET_ESTIMATED_CONNECTED_TRAIN_ROUTE,
- { variables: connectedVariables, skip: skipConnected }
+ {
+ variables: connectedVariables,
+ skip: skipConnected || !estimatedEnabled,
+ }
);
const { data: legacyConnectedData, error: legacyConnectedError } =
useGraphQLQuery(
GET_CONNECTED_TRAIN_ROUTE,
{
variables: connectedVariables,
- skip: skipConnected || !estimatedConnectedError,
+ skip: skipConnected || (estimatedEnabled && !estimatedConnectedError),
}
);
- const singleTrainRouteData: TrainRouteData | undefined = estimatedSingleError
+ // skip していても同じ queryKey のキャッシュがあれば data が返るので、どちらを使うかは
+ // フラグとエラーで決める
+ const useLegacySingle = !estimatedEnabled || !!estimatedSingleError;
+ const useLegacyConnected = !estimatedEnabled || !!estimatedConnectedError;
+ const singleTrainRouteData: TrainRouteData | undefined = useLegacySingle
? legacySingleData
: estimatedSingleData;
- const connectedTrainRouteData = estimatedConnectedError
+ const connectedTrainRouteData = useLegacyConnected
? legacyConnectedData
: estimatedConnectedData;
- const estimatedTrainRouteError = routeLegs
- ? estimatedConnectedError
- : estimatedSingleError;
+ const estimatedTrainRouteError = !estimatedEnabled
+ ? undefined
+ : routeLegs
+ ? estimatedConnectedError
+ : estimatedSingleError;
const legacyTrainRouteError = routeLegs
? legacyConnectedError
: legacySingleError;
diff --git a/src/lib/remoteConfig.test.ts b/src/lib/remoteConfig.test.ts
index ca1b122a53..0287766ada 100644
--- a/src/lib/remoteConfig.test.ts
+++ b/src/lib/remoteConfig.test.ts
@@ -7,6 +7,7 @@ import {
getVoicevoxTTSManifestUrl,
getVoicevoxTTSStyleId,
isAIAgentFeatureEnabled,
+ isAutoModeEstimatedEnabled,
isEtaAssistEnabled,
isRemoteTTSEnabled,
isTTSFeatureEnabled,
@@ -596,6 +597,38 @@ describe('isAIAgentFeatureEnabled(サーバー側キルスイッチ)', () =>
});
});
+describe('isAutoModeEstimatedEnabled(オートモードの走らせ方)', () => {
+ it('取得前は従来の値(false)', () => {
+ expect(isAutoModeEstimatedEnabled()).toBe(false);
+ });
+
+ it('true が配信されたら到着時間推定の見込みで走る', async () => {
+ mockRemoteConfig({ auto_mode_estimated_enabled: true });
+ await setupRemoteConfig();
+ expect(isAutoModeEstimatedEnabled()).toBe(true);
+ });
+
+ it('false が配信されたら従来の値で走る', async () => {
+ mockRemoteConfig({ auto_mode_estimated_enabled: false });
+ await setupRemoteConfig();
+ expect(isAutoModeEstimatedEnabled()).toBe(false);
+ });
+
+ it('真偽値でなければ従来の値に倒す', async () => {
+ mockRemoteConfig({ auto_mode_estimated_enabled: 'true' });
+ await setupRemoteConfig();
+ expect(isAutoModeEstimatedEnabled()).toBe(false);
+ });
+
+ it('取得に失敗したら従来の値に倒す', async () => {
+ mockRemoteConfig({}, false);
+ await expect(setupRemoteConfig()).rejects.toThrow(
+ 'remote config fetch failed: 503'
+ );
+ expect(isAutoModeEstimatedEnabled()).toBe(false);
+ });
+});
+
describe('subscribeRemoteConfig(キャッシュ更新の購読)', () => {
it('setupRemoteConfig 完了時にリスナーへ通知される', async () => {
const listener = jest.fn();
diff --git a/src/lib/remoteConfig.ts b/src/lib/remoteConfig.ts
index 40e1c91e7b..d7ca5921aa 100644
--- a/src/lib/remoteConfig.ts
+++ b/src/lib/remoteConfig.ts
@@ -56,6 +56,9 @@ export const REMOTE_CONFIG_KEYS = {
// AIエージェント(行き先相談)機能の有効/無効。障害・コスト超過時にサーバー側から
// エントリポイントごと機能を止められるようにするキルスイッチ。
AI_AGENT_ENABLED: 'ai_agent_enabled',
+ // オートモードを到着時間推定の見込み(trainRoute の model: Estimated)で走らせるかどうか。
+ // true なら ETA と同じ時間で駅に着いて停まり、false・未配信なら従来の値(Legacy)で走る。
+ AUTO_MODE_ESTIMATED_ENABLED: 'auto_mode_estimated_enabled',
} as const;
type RemoteConfigResponse = {
@@ -75,6 +78,7 @@ type RemoteConfigResponse = {
vits_tts_enabled_ios?: boolean;
vits_tts_manifest_url_ios?: string;
ai_agent_enabled?: boolean;
+ auto_mode_estimated_enabled?: boolean;
};
// ETAフォールバック機能自体のフォールバック既定値。安全側に倒し、既定では無効とする。
@@ -99,6 +103,10 @@ const REMOTE_TTS_ENABLED_ANDROID_FALLBACK = false;
// 未配信・取得失敗時は安全側に倒して無効とする。
const AI_AGENT_ENABLED_FALLBACK = false;
+// オートモードの走らせ方のフォールバック既定値。未配信・取得失敗時は従来の値(Legacy)で
+// 走らせ、配信で true にしたときだけ到着時間推定の見込みに切り替える。
+const AUTO_MODE_ESTIMATED_ENABLED_FALLBACK = false;
+
// VOICEVOX フォールバックのフォールバック既定値。約 180MB の辞書・音声モデルを
// 端末へ取得する機能のため、Remote Config で明示的に有効化されたときだけ動かす。
const VOICEVOX_TTS_ENABLED_IOS_FALLBACK = false;
@@ -149,6 +157,7 @@ let cachedTTSEnabledAndroid: boolean | null = null;
let cachedRemoteTTSEnabledIOS: boolean | null = null;
let cachedRemoteTTSEnabledAndroid: boolean | null = null;
let cachedAIAgentEnabled: boolean | null = null;
+let cachedAutoModeEstimatedEnabled: boolean | null = null;
let cachedVoicevoxTTSEnabledIOS: boolean | null = null;
let cachedVoicevoxTTSManifestUrlIOS: string | null = null;
let cachedVoicevoxTTSStyleIdIOS: number | null = null;
@@ -185,6 +194,7 @@ export const resetRemoteConfigCache = (): void => {
cachedRemoteTTSEnabledIOS = null;
cachedRemoteTTSEnabledAndroid = null;
cachedAIAgentEnabled = null;
+ cachedAutoModeEstimatedEnabled = null;
cachedVoicevoxTTSEnabledIOS = null;
cachedVoicevoxTTSManifestUrlIOS = null;
cachedVoicevoxTTSStyleIdIOS = null;
@@ -239,6 +249,9 @@ export const setupRemoteConfig = async (): Promise => {
if (typeof data.ai_agent_enabled === 'boolean') {
cachedAIAgentEnabled = data.ai_agent_enabled;
}
+ if (typeof data.auto_mode_estimated_enabled === 'boolean') {
+ cachedAutoModeEstimatedEnabled = data.auto_mode_estimated_enabled;
+ }
if (typeof data.voicevox_tts_enabled_ios === 'boolean') {
cachedVoicevoxTTSEnabledIOS = data.voicevox_tts_enabled_ios;
}
@@ -358,6 +371,16 @@ export const isAIAgentFeatureEnabled = (): boolean => {
return AI_AGENT_ENABLED_FALLBACK;
};
+// オートモードを到着時間推定の見込み(trainRoute の model: Estimated)で走らせるかどうかを
+// 同期的に取得する。setupRemoteConfig 完了後は取得済みのリモート値を、未設定・取得失敗時は
+// フォールバック(false=従来の値で走る)を返す。
+export const isAutoModeEstimatedEnabled = (): boolean => {
+ if (cachedAutoModeEstimatedEnabled != null) {
+ return cachedAutoModeEstimatedEnabled;
+ }
+ return AUTO_MODE_ESTIMATED_ENABLED_FALLBACK;
+};
+
/**
* iOS でリモート TTS が使えない回の日本語フォールバックを VOICEVOX (端末内合成) で
* 読み上げるかどうかを同期的に取得する。isRemoteTTSEnabled とは独立しており、