- 1816
assert!(out[1].as_ref().err().unwrap().contains("unknown service")); - 1817
- 1818
let synced = services_sync(Path::new("/b"), &["nope"], &Paths::default(), &Fake::new()); - 1819
assert!(matches!(synced[0].action, SyncAction::Failed(_))); - 1820
} - 1821
- 1822
fn write_bots_json(data_home: &Path, bots: &[(&str, &str)]) { - 1823
let dir = data_home.join("gateway"); - 1824
std::fs::create_dir_all(&dir).unwrap(); - 1825
let entries: Vec<String> = bots - 1826
.iter() - 1827
.map(|(id, surface)| format!(r#"{{"id":"{id}","surface":"{surface}"}}"#)) - 1828
.collect(); - 1829
std::fs::write( - 1830
dir.join("bots.json"), - 1831
format!(r#"{{"schema":1,"bots":[{}]}}"#, entries.join(",")), - 1832
) - 1833
.unwrap(); - 1834
} - 1835
- 1836
/// One unit per configured bot, named and argued so it resolves that - 1837
/// bot's own token — the fix for the multi-bot regression where a - 1838
/// single static `com.vak.telegram` unit with no `--bot-id` fell back - 1839
/// to the dead legacy `TELEGRAM_BOT_TOKEN` slot. - 1840
#[test] - 1841
fn bot_service_specs_one_per_configured_bot_with_bot_id_arg() { - 1842
let dir = tempfile::tempdir().unwrap(); - 1843
write_bots_json( - 1844
dir.path(), - 1845
&[("VakBot", "telegram"), ("VakyarthaBot", "telegram")], - 1846
); - 1847
let specs = bot_service_specs( - 1848
Path::new("/opt/vak/bin"), - 1849
Path::new("/Users/x"), - 1850
Path::new("/workspace"), - 1851
dir.path(), - 1852
"http://127.0.0.1:8901", - 1853
); - 1854
assert_eq!(specs.len(), 2); - 1855
let names: Vec<&str> = specs.iter().map(|s| s.name.as_str()).collect(); - 1856
assert!(names.contains(&"com.vak.telegram-VakBot")); - 1857
assert!(names.contains(&"com.vak.telegram-VakyarthaBot")); - 1858
let vakbot = specs - 1859
.iter() - 1860
.find(|s| s.name == "com.vak.telegram-VakBot") - 1861
.unwrap(); - 1862
assert_eq!( - 1863
vakbot.args, - 1864
vec![ - 1865
"telegram", - 1866
"--server", - 1867
"http://127.0.0.1:8901", - 1868
"--bot-id", - 1869
"VakBot" - 1870
], - 1871
); - 1872
assert!(vakbot.keep_alive); - 1873
assert_eq!(vakbot.working_dir, Path::new("/workspace")); - 1874
// Every rendered unit must still carry zero secrets — the token - 1875
// lives only in the env var the bridge process reads for itself. - 1876
let plist = render_launchd_plist(vakbot); - 1877
for secret in ["TOKEN", "SECRET", "BOT_TOKEN"] { - 1878
assert!(!plist.contains(secret), "unit must not contain {secret}"); - 1879
} - 1880
} - 1881
- 1882
/// A surface the CLI has no bridge for (or a bots.json typo) must not - 1883
/// produce a unit that can never run. - 1884
#[test] - 1885
fn bot_service_specs_skips_unknown_surfaces_and_missing_file() { - 1886
let dir = tempfile::tempdir().unwrap(); - 1887
write_bots_json(dir.path(), &[("Weird", "carrier-pigeon")]); - 1888
let specs = bot_service_specs( - 1889
Path::new("/b"), - 1890
Path::new("/h"), - 1891
Path::new("/w"), - 1892
dir.path(), - 1893
"http://127.0.0.1:8901", - 1894
); - 1895
assert!(specs.is_empty()); - 1896
- 1897
let missing = tempfile::tempdir().unwrap(); - 1898
let specs = bot_service_specs( - 1899
Path::new("/b"), - 1900
Path::new("/h"), - 1901
Path::new("/w"), - 1902
missing.path(), - 1903
"http://127.0.0.1:8901", - 1904
); - 1905
assert!( - 1906
specs.is_empty(), - 1907
"no bots.json yet must mean no units, not an error" - 1908
); - 1909
} - 1910
- 1911
/// Deleting (or renaming) a bot must take its bridge process down too — - 1912
/// otherwise it keeps running forever against a token env var nothing - 1913
/// references any more. - 1914
#[test] - 1915
fn sync_bots_prunes_units_for_deleted_bots() { - 1916
let (_d, paths) = tmp_paths("bots-prune"); - 1917
let data = tempfile::tempdir().unwrap(); - 1918
let fake = Fake::with_pid(123); - 1919
- 1920
write_bots_json( - 1921
data.path(), - 1922
&[("VakBot", "telegram"), ("Second", "telegram")], - 1923
); - 1924
let first = sync_bots( - 1925
Path::new("/opt/vak/bin/vak"), - 1926
data.path(), - 1927
"http://127.0.0.1:8901", - 1928
&paths, - 1929
&fake, - 1930
); - 1931
assert_eq!(first.len(), 2); - 1932
assert!(unit_file_path("com.vak.telegram-VakBot", &paths).exists()); - 1933
assert!(unit_file_path("com.vak.telegram-Second", &paths).exists()); - 1934
- 1935
// User deletes "Second" from the admin console. - 1936
write_bots_json(data.path(), &[("VakBot", "telegram")]); - 1937
let second = sync_bots( - 1938
Path::new("/opt/vak/bin/vak"), - 1939
data.path(), - 1940
"http://127.0.0.1:8901", - 1941
&paths, - 1942
&fake, - 1943
); - 1944
assert_eq!( - 1945
second.len(), - 1946
1, - 1947
"only the surviving bot should be (re)synced" - 1948
); - 1949
assert!( - 1950
unit_file_path("com.vak.telegram-VakBot", &paths).exists(), - 1951
"surviving bot's unit must be untouched" - 1952
); - 1953
assert!( - 1954
!unit_file_path("com.vak.telegram-Second", &paths).exists(), - 1955
"deleted bot's unit must be removed, not left running forever" - 1956
); - 1957
} - 1958
- 1959
#[test] - 1960
fn bot_service_name_sanitizes_and_namespaces_by_surface() { - 1961
assert_eq!( - 1962
bot_service_name("telegram", "VakBot"), - 1963
"com.vak.telegram-VakBot" - 1964
); - 1965
assert_eq!( - 1966
bot_service_name("discord", "weird id!"), - 1967
"com.vak.discord-weird_id_" - 1968
); - 1969
} - 1970
- 1971
/// An uninstall must not reach into another install's units. - 1972
/// - 1973
/// The defect: `vak self uninstall --prefix <tmp>` tore down units by - 1974
/// global name against the real units directory, so running the test - 1975
/// suite on a machine with vak installed silently stopped its - 1976
/// services. Ownership is decided by what the unit actually execs. - 1977
#[test] - 1978
fn a_unit_belongs_only_to_the_prefix_it_execs_from() { - 1979
let units = tempfile::tempdir().unwrap(); - 1980
let paths = Paths { - 1981
launch_agents_dir: units.path().to_path_buf(), - 1982
systemd_unit_dir: units.path().to_path_buf(), - 1983
}; - 1984
let name = "com.vak.gateway"; - 1985
std::fs::write( - 1986
unit_file_path(name, &paths), - 1987
"ExecStart=/Applications/Vak.app/Contents/MacOS/vak serve\n", - 1988
) - 1989
.unwrap(); - 1990
- 1991
assert!( - 1992
unit_belongs_to_prefix(name, Path::new("/Applications/Vak.app"), &paths), - 1993
"the real install owns its own unit" - 1994
); - 1995
assert!( - 1996
!unit_belongs_to_prefix(name, Path::new("/tmp/throwaway-prefix"), &paths), - 1997
"a throwaway prefix must not claim a unit it does not exec" - 1998
); - 1999
assert!( - 2000
!unit_belongs_to_prefix("com.vak.absent", Path::new("/anything"), &paths), - 2001
"a unit that does not exist is owned by nobody" - 2002
); - 2003
} - 2004
- 2005
/// Configured and activated are different states, and the difference - 2006
/// has to be visible: a bot in `bots.json` with no unit file is one an - 2007
/// operator created but has not activated yet, not a fault. - 2008
#[test] - 2009
fn a_configured_bot_without_a_unit_reads_as_not_registered() { - 2010
let data = tempfile::tempdir().unwrap(); - 2011
let units = tempfile::tempdir().unwrap(); - 2012
let paths = Paths { - 2013
launch_agents_dir: units.path().to_path_buf(), - 2014
systemd_unit_dir: units.path().to_path_buf(), - 2015
}; - 2016
assert!(configured_bot_service_names_all(data.path()).is_empty()); - 2017
- 2018
write_bots_json(data.path(), &[("VakBot", "telegram")]); - 2019
let names = configured_bot_service_names_all(data.path()); - 2020
assert_eq!(names, vec!["com.vak.telegram-VakBot"]); - 2021
assert!( - 2022
!unit_is_registered(&names[0], &paths), - 2023
"configured is not activated" - 2024
); - 2025
- 2026
std::fs::write(unit_file_path(&names[0], &paths), b"unit").unwrap(); - 2027
assert!(unit_is_registered(&names[0], &paths)); - 2028
} - 2029
- 2030
#[test] - 2031
fn configured_bot_service_names_follow_the_live_bot_store() { - 2032
let data = tempfile::tempdir().unwrap(); - 2033
write_bots_json(data.path(), &[("VakBot", "telegram"), ("Other", "discord")]); - 2034
assert_eq!( - 2035
configured_bot_service_names(data.path(), "telegram"), - 2036
vec!["com.vak.telegram-VakBot"] - 2037
); - 2038
} - 2039
- 2040
#[test] - 2041
fn uninstall_bot_units_removes_every_bot_unit_regardless_of_bots_json() { - 2042
let (_d, paths) = tmp_paths("bots-uninstall"); - 2043
let data = tempfile::tempdir().unwrap(); - 2044
let fake = Fake::with_pid(123); - 2045
- 2046
write_bots_json(data.path(), &[("VakBot", "telegram"), ("Ops", "discord")]); - 2047
sync_bots( - 2048
Path::new("/opt/vak/bin/vak"), - 2049
data.path(), - 2050
"http://127.0.0.1:8901", - 2051
&paths, - 2052
&fake, - 2053
); - 2054
assert!(unit_file_path("com.vak.telegram-VakBot", &paths).exists()); - 2055
assert!(unit_file_path("com.vak.discord-Ops", &paths).exists()); - 2056
- 2057
// Uninstall must remove every bot unit even though bots.json still - 2058
// lists them — an uninstall wants "wanted = nothing", not a diff - 2059
// against the still-present config file. - 2060
uninstall_bot_units(&paths, &fake); - 2061
assert!(!unit_file_path("com.vak.telegram-VakBot", &paths).exists()); - 2062
assert!(!unit_file_path("com.vak.discord-Ops", &paths).exists()); - 2063
} - 2064
} - 2065
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.