11import * as assert from "node:assert" ;
2+ import * as os from "node:os" ;
23import * as path from "node:path" ;
34import * as vscode from "vscode" ;
5+ import { MiseService } from "../../miseService" ;
6+ import { getBootstrapSections } from "../../utils/bootstrapUtils" ;
47
58/**
69 * Bootstrap e2e tests. Everything here is read-only and machine-agnostic:
7- * `mise bootstrap` is never executed (only definition navigation and command
10+ * `mise bootstrap` is never executed (only status, `bootstrap plan` — which is
11+ * a preview and never applies anything — definition navigation and command
812 * registration are exercised), the fixture entries point at resources that do
913 * not exist anywhere, and the suite isolates the machine's global mise config
1014 * via MISE_GLOBAL_CONFIG_FILE (see .vscode-test.js), so results are identical
@@ -16,6 +20,21 @@ suite("Bootstrap Test Suite", function () {
1620 let workspaceRoot : string ;
1721 let miseTomlDocument : vscode . TextDocument ;
1822
23+ // the service only reads workspaceState from the context
24+ const fakeContext = {
25+ workspaceState : { get : ( ) => undefined } ,
26+ } as unknown as vscode . ExtensionContext ;
27+
28+ const createMiseService = async ( ) => {
29+ const miseService = new MiseService ( fakeContext ) ;
30+ await miseService . initializeMisePath ( ) ;
31+ assert . ok ( miseService . getMiseBinaryPath ( ) , "mise binary should resolve" ) ;
32+ return miseService ;
33+ } ;
34+
35+ // mise reports resource paths expanded, the fixture declares them with `~`
36+ const fixturePath = ( name : string ) => path . join ( os . homedir ( ) , name ) ;
37+
1938 const lineOf = ( text : string ) : number => {
2039 for ( let i = 0 ; i < miseTomlDocument . lineCount ; i ++ ) {
2140 if ( miseTomlDocument . lineAt ( i ) . text . includes ( text ) ) {
@@ -55,6 +74,7 @@ suite("Bootstrap Test Suite", function () {
5574 for ( const command of [
5675 "mise.runBootstrap" ,
5776 "mise.runBootstrapDryRun" ,
77+ "mise.runBootstrapPlan" ,
5878 "mise.showBootstrap" ,
5979 "mise.openBootstrapEntryDefinition" ,
6080 ] ) {
@@ -157,4 +177,126 @@ suite("Bootstrap Test Suite", function () {
157177 test ( "show bootstrap status webview opens without error" , async ( ) => {
158178 await vscode . commands . executeCommand ( "mise.showBootstrap" ) ;
159179 } ) ;
180+
181+ test ( "status reports the declarative resource sections" , async ( ) => {
182+ const miseService = await createMiseService ( ) ;
183+
184+ const status = await miseService . getBootstrapStatus ( ) ;
185+ assert . ok ( status , "bootstrap status should be returned" ) ;
186+
187+ const sections = getBootstrapSections ( status ) ;
188+ const byLabel = ( label : string ) =>
189+ sections . find ( ( section ) => section . label === label ) ;
190+
191+ const files = byLabel ( "Files" ) ;
192+ assert . ok ( files , "Files section should be present" ) ;
193+ assert . deepEqual (
194+ files . entries . map ( ( entry ) => entry . label ) ,
195+ [
196+ fixturePath ( ".mise-vscode-e2e-dir" ) ,
197+ fixturePath ( ".mise-vscode-e2e-dir-two" ) ,
198+ fixturePath ( ".mise-vscode-e2e-file.conf" ) ,
199+ fixturePath ( ".mise-vscode-e2e-secret.conf" ) ,
200+ ] ,
201+ "directories and files share one section, in mise's order" ,
202+ ) ;
203+ assert . equal ( files . entries [ 2 ] ?. state , "create" ) ;
204+ // the secret this file templates is never set, so mise cannot inspect it
205+ assert . equal ( files . entries [ 3 ] ?. state , "unknown" ) ;
206+
207+ const secrets = byLabel ( "Secrets" ) ;
208+ assert . ok ( secrets , "Secrets section should be present" ) ;
209+ assert . equal ( secrets . entries [ 0 ] ?. label , "e2e_token" ) ;
210+ assert . equal (
211+ secrets . entries [ 0 ] ?. description ,
212+ "MISE_VSCODE_E2E_SECRET_THAT_IS_NEVER_SET" ,
213+ ) ;
214+ assert . equal ( secrets . entries [ 0 ] ?. state , "missing" ) ;
215+
216+ // the fixture declares no services, firewall, compose or accounts, so
217+ // those sections stay out of the view on every platform
218+ for ( const label of [ "Services" , "Firewall" , "Compose" , "Accounts" ] ) {
219+ assert . equal ( byLabel ( label ) , undefined , `${ label } should be absent` ) ;
220+ }
221+ } ) ;
222+
223+ test ( "plan previews the changes without applying them" , async ( ) => {
224+ const miseService = await createMiseService ( ) ;
225+ assert . ok (
226+ await miseService . isBootstrapPlanAvailable ( ) ,
227+ "`mise bootstrap plan` requires mise 2026.8.2 or later" ,
228+ ) ;
229+
230+ const plan = await miseService . getBootstrapPlan ( ) ;
231+ assert . ok ( plan , "bootstrap plan should be returned" ) ;
232+
233+ assert . deepEqual (
234+ plan . resources . map ( ( resource ) => resource . id ) ,
235+ [
236+ { kind : "directory" , name : fixturePath ( ".mise-vscode-e2e-dir" ) } ,
237+ { kind : "directory" , name : fixturePath ( ".mise-vscode-e2e-dir-two" ) } ,
238+ { kind : "file" , name : fixturePath ( ".mise-vscode-e2e-file.conf" ) } ,
239+ { kind : "file" , name : fixturePath ( ".mise-vscode-e2e-secret.conf" ) } ,
240+ ] ,
241+ ) ;
242+ assert . deepEqual ( plan . summary , {
243+ create : 3 ,
244+ update : 0 ,
245+ remove : 0 ,
246+ unchanged : 0 ,
247+ unknown : 1 ,
248+ } ) ;
249+
250+ // a plan is a preview: nothing it lists may exist afterwards
251+ for ( const resource of plan . resources ) {
252+ assert . equal (
253+ await vscode . workspace . fs . stat ( vscode . Uri . file ( resource . id . name ) ) . then (
254+ ( ) => true ,
255+ ( ) => false ,
256+ ) ,
257+ false ,
258+ `${ resource . id . name } should not have been created` ,
259+ ) ;
260+ }
261+ } ) ;
262+
263+ // status reports resource paths expanded, so navigation has to find the `~`
264+ // form they are declared with. Both tests target the second declaration of
265+ // their kind: the enclosing-table fallback always resolves to the first, so
266+ // they fail unless the expanded path is really matched back to its `~` form.
267+ test ( "navigates to a [bootstrap.files] declaration from the expanded path" , async ( ) => {
268+ const editor = await openDefinition (
269+ [ "bootstrap" , "files" ] ,
270+ fixturePath ( ".mise-vscode-e2e-secret.conf" ) ,
271+ ) ;
272+
273+ assert . equal (
274+ editor . selection . start . line ,
275+ lineOf ( '"~/.mise-vscode-e2e-secret.conf"' ) ,
276+ "Selection should be on the file declaration line" ,
277+ ) ;
278+ } ) ;
279+
280+ test ( "navigates to a [bootstrap.directories] declaration from the expanded path" , async ( ) => {
281+ const editor = await openDefinition (
282+ [ "bootstrap" , "directories" ] ,
283+ fixturePath ( ".mise-vscode-e2e-dir-two" ) ,
284+ ) ;
285+
286+ assert . equal (
287+ editor . selection . start . line ,
288+ lineOf ( '"~/.mise-vscode-e2e-dir-two"' ) ,
289+ "Selection should be on the directory declaration line" ,
290+ ) ;
291+ } ) ;
292+
293+ test ( "navigates to a [bootstrap.secrets] declaration" , async ( ) => {
294+ const editor = await openDefinition ( [ "bootstrap" , "secrets" ] , "e2e_token" ) ;
295+
296+ assert . equal (
297+ editor . selection . start . line ,
298+ lineOf ( "e2e_token =" ) ,
299+ "Selection should be on the secret declaration line" ,
300+ ) ;
301+ } ) ;
160302} ) ;
0 commit comments