@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
55import { listInstalledServices , restartCommand , type InstalledService } from "./service-restart.ts" ;
6+ import { generateServiceFile , parseInstalledServiceConfig , type ServiceConfig } from "./service.ts" ;
67
78const cleanups : Array < ( ) => Promise < void > > = [ ] ;
89
@@ -58,6 +59,88 @@ describe("listInstalledServices", () => {
5859 } ) ;
5960} ) ;
6061
62+ describe ( "parseInstalledServiceConfig" , ( ) => {
63+ it ( "reconstructs a ServiceConfig from a systemd unit written by generateServiceFile" , async ( ) => {
64+ const dir = await makeTempDir ( ) ;
65+ const filePath = join ( dir , "webmux-roundtrip.service" ) ;
66+ const original : ServiceConfig = {
67+ platform : "linux" ,
68+ projectName : "roundtrip" ,
69+ serviceName : "webmux-roundtrip" ,
70+ webmuxPath : "/usr/local/bin/webmux" ,
71+ projectDir : dir ,
72+ port : 5117 ,
73+ } ;
74+ await writeFile ( filePath , generateServiceFile ( original ) ) ;
75+
76+ const parsed = parseInstalledServiceConfig ( filePath , "linux" , "/new/path/webmux" ) ;
77+
78+ expect ( parsed ) . not . toBeNull ( ) ;
79+ expect ( parsed ?. port ) . toBe ( 5117 ) ;
80+ expect ( parsed ?. projectDir ) . toBe ( dir ) ;
81+ expect ( parsed ?. serviceName ) . toBe ( "webmux-roundtrip" ) ;
82+ // webmuxPath comes from the caller (post-upgrade `which webmux`), not the unit.
83+ expect ( parsed ?. webmuxPath ) . toBe ( "/new/path/webmux" ) ;
84+ } ) ;
85+
86+ it ( "reconstructs a ServiceConfig from a launchd plist written by generateServiceFile" , async ( ) => {
87+ const dir = await makeTempDir ( ) ;
88+ const filePath = join ( dir , "com.webmux.webmux-roundtrip.plist" ) ;
89+ const original : ServiceConfig = {
90+ platform : "darwin" ,
91+ projectName : "roundtrip" ,
92+ serviceName : "webmux-roundtrip" ,
93+ webmuxPath : "/usr/local/bin/webmux" ,
94+ projectDir : dir ,
95+ port : 5222 ,
96+ } ;
97+ await writeFile ( filePath , generateServiceFile ( original ) ) ;
98+
99+ const parsed = parseInstalledServiceConfig ( filePath , "darwin" , "/new/path/webmux" ) ;
100+
101+ expect ( parsed ) . not . toBeNull ( ) ;
102+ expect ( parsed ?. port ) . toBe ( 5222 ) ;
103+ expect ( parsed ?. projectDir ) . toBe ( dir ) ;
104+ expect ( parsed ?. serviceName ) . toBe ( "webmux-roundtrip" ) ;
105+ } ) ;
106+
107+ it ( "returns null when the unit file lacks --port" , async ( ) => {
108+ const dir = await makeTempDir ( ) ;
109+ const filePath = join ( dir , "broken.service" ) ;
110+ await writeFile ( filePath , "[Service]\nWorkingDirectory=/x\n" ) ;
111+ expect ( parseInstalledServiceConfig ( filePath , "linux" , "/path/webmux" ) ) . toBeNull ( ) ;
112+ } ) ;
113+ } ) ;
114+
115+ describe ( "generateServiceFile → parseInstalledServiceConfig → generateServiceFile is idempotent" , ( ) => {
116+ it ( "regenerated content matches the original for systemd units" , async ( ) => {
117+ const dir = await makeTempDir ( ) ;
118+ // `detectProjectName` reads package.json's `name` first, so seeding one
119+ // with a stable name guarantees the re-derived `projectName` matches the
120+ // original — otherwise it would fall back to the random temp-dir basename
121+ // and the round-trip would diverge on the `Description=` line.
122+ await writeFile ( join ( dir , "package.json" ) , JSON . stringify ( { name : "idempotent" } ) ) ;
123+ const filePath = join ( dir , "webmux-idempotent.service" ) ;
124+ const original : ServiceConfig = {
125+ platform : "linux" ,
126+ projectName : "idempotent" ,
127+ serviceName : "webmux-idempotent" ,
128+ webmuxPath : "/usr/local/bin/webmux" ,
129+ projectDir : dir ,
130+ port : 5333 ,
131+ } ;
132+ const originalContent = generateServiceFile ( original ) ;
133+ await writeFile ( filePath , originalContent ) ;
134+
135+ const parsed = parseInstalledServiceConfig ( filePath , "linux" , "/usr/local/bin/webmux" ) ;
136+ expect ( parsed ) . not . toBeNull ( ) ;
137+ if ( ! parsed ) throw new Error ( "parse failed" ) ;
138+
139+ // Round-trip should produce identical content when webmuxPath is unchanged.
140+ expect ( generateServiceFile ( parsed ) ) . toBe ( originalContent ) ;
141+ } ) ;
142+ } ) ;
143+
61144describe ( "restartCommand" , ( ) => {
62145 it ( "builds the systemctl --user restart command for linux" , ( ) => {
63146 const svc : InstalledService = {
0 commit comments