@@ -3,7 +3,13 @@ import { tmpdir } from 'node:os'
33import path from 'node:path'
44import { describe , expect , it } from 'vitest'
55import { ensureProductionComposeFile } from './compose-asset'
6- import { composeInstallFromDirectory } from './lifecycle'
6+ import {
7+ composeInstallFromDirectory ,
8+ composeServiceQuery ,
9+ composeServiceState ,
10+ type Install ,
11+ serviceStatusRows ,
12+ } from './lifecycle'
713
814describe ( 'setup lifecycle' , ( ) => {
915 it ( 'does not duplicate a running install restored from its directory' , ( ) => {
@@ -17,4 +23,111 @@ describe('setup lifecycle', () => {
1723 rmSync ( root , { recursive : true , force : true } )
1824 }
1925 } )
26+
27+ it ( 'reads a Compose service as running when any replica is running' , ( ) => {
28+ expect ( composeServiceState ( 'running' ) ) . toEqual ( { state : 'running' } )
29+ expect ( composeServiceState ( 'exited\nrunning\n' ) ) . toEqual ( { state : 'running' } )
30+ } )
31+
32+ it ( 'reads a Compose service with only stopped containers as stopped' , ( ) => {
33+ expect ( composeServiceState ( 'exited' ) ) . toEqual ( { state : 'stopped' } )
34+ expect ( composeServiceState ( 'created\nexited' ) ) . toEqual ( { state : 'stopped' } )
35+ } )
36+
37+ it ( 'reads an empty Compose query as absent and a failed one as unknown' , ( ) => {
38+ expect ( composeServiceState ( '' ) ) . toBeNull ( )
39+ expect ( composeServiceState ( '\n \n' ) ) . toBeNull ( )
40+ expect ( composeServiceState ( null ) ) . toBe ( 'unknown' )
41+ } )
42+ } )
43+
44+ describe ( 'status service rows' , ( ) => {
45+ const compose = ( project : string ) : Install => ( {
46+ kind : 'compose' ,
47+ file : `/srv/${ project } /docker-compose.prod.yml` ,
48+ dir : `/srv/${ project } ` ,
49+ project,
50+ } )
51+ const dev : Install = { kind : 'dev' , postgres : true , redis : true }
52+ const standalone = { db : { state : 'running' as const } , redis : null }
53+
54+ /** Answers `docker ps` by the project/service labels in the query, like Docker would. */
55+ const fakeDocker =
56+ ( states : Record < string , string | null > ) =>
57+ ( args : string [ ] ) : string | null => {
58+ const project = args . find ( ( a ) => a . startsWith ( 'label=com.docker.compose.project=' ) )
59+ const service = args . find ( ( a ) => a . startsWith ( 'label=com.docker.compose.service=' ) )
60+ const key = `${ project ?. split ( '=' ) . pop ( ) } /${ service ?. split ( '=' ) . pop ( ) } `
61+ return key in states ? states [ key ] ! : ''
62+ }
63+
64+ it ( 'queries Compose services by project and service label' , ( ) => {
65+ expect ( composeServiceQuery ( 'sim-abc' , 'db' ) ) . toEqual ( [
66+ 'ps' ,
67+ '-a' ,
68+ '--filter' ,
69+ 'label=com.docker.compose.project=sim-abc' ,
70+ '--filter' ,
71+ 'label=com.docker.compose.service=db' ,
72+ '--format' ,
73+ '{{.State}}' ,
74+ ] )
75+ } )
76+
77+ it ( 'shows only Compose rows for a Compose install' , ( ) => {
78+ const rows = serviceStatusRows (
79+ [ compose ( 'sim-abc' ) ] ,
80+ standalone ,
81+ fakeDocker ( { 'sim-abc/db' : 'running' , 'sim-abc/redis' : 'exited' } )
82+ )
83+ expect ( rows ) . toEqual ( [
84+ { label : 'postgres (compose db)' , state : { state : 'running' } } ,
85+ { label : 'redis (compose redis)' , state : { state : 'stopped' } } ,
86+ ] )
87+ } )
88+
89+ it ( 'reports a failed Compose query as unknown, not absent' , ( ) => {
90+ const rows = serviceStatusRows ( [ compose ( 'sim-abc' ) ] , standalone , ( ) => null )
91+ expect ( rows . map ( ( row ) => row . state ) ) . toEqual ( [ 'unknown' , 'unknown' ] )
92+ } )
93+
94+ it ( 'keeps standalone rows when a dev install is present' , ( ) => {
95+ const rows = serviceStatusRows (
96+ [ dev , compose ( 'sim-abc' ) ] ,
97+ standalone ,
98+ fakeDocker ( { 'sim-abc/db' : 'running' , 'sim-abc/redis' : 'running' } )
99+ )
100+ expect ( rows . map ( ( row ) => row . label ) ) . toEqual ( [
101+ 'postgres (sim-postgres)' ,
102+ 'redis (sim-redis)' ,
103+ 'postgres (compose db)' ,
104+ 'redis (compose redis)' ,
105+ ] )
106+ expect ( rows [ 0 ] ! . state ) . toEqual ( { state : 'running' } )
107+ expect ( rows [ 1 ] ! . state ) . toBeNull ( )
108+ } )
109+
110+ it ( 'never queries Compose labels without a Compose install' , ( ) => {
111+ let queried = false
112+ const rows = serviceStatusRows ( [ dev ] , standalone , ( ) => {
113+ queried = true
114+ return ''
115+ } )
116+ expect ( queried ) . toBe ( false )
117+ expect ( rows . map ( ( row ) => row . label ) ) . toEqual ( [ 'postgres (sim-postgres)' , 'redis (sim-redis)' ] )
118+ } )
119+
120+ it ( 'prefixes rows with the project when several Compose stacks run' , ( ) => {
121+ const rows = serviceStatusRows (
122+ [ compose ( 'sim-a' ) , compose ( 'sim-b' ) ] ,
123+ standalone ,
124+ fakeDocker ( { 'sim-a/db' : 'running' , 'sim-b/db' : '' } )
125+ )
126+ expect ( rows . map ( ( row ) => [ row . label , row . state ] ) ) . toEqual ( [
127+ [ 'sim-a postgres (compose db)' , { state : 'running' } ] ,
128+ [ 'sim-a redis (compose redis)' , null ] ,
129+ [ 'sim-b postgres (compose db)' , null ] ,
130+ [ 'sim-b redis (compose redis)' , null ] ,
131+ ] )
132+ } )
20133} )
0 commit comments