@@ -23,6 +23,13 @@ const FIXTURE_KERNEL_ENV = {
2323 'tests' ,
2424 'fixtures' ,
2525 'fake-kernel-sibling'
26+ ) ,
27+ VIBE_SCIENCE_PLUGIN_CLI : path . join (
28+ repoRoot ,
29+ 'environment' ,
30+ 'tests' ,
31+ 'fixtures' ,
32+ 'governance-log-capture-stub.js'
2633 )
2734} ;
2835
@@ -71,6 +78,33 @@ async function readJson(targetPath) {
7178 return JSON . parse ( await readFile ( targetPath , 'utf8' ) ) ;
7279}
7380
81+ async function readGovernanceEvents ( capturePath ) {
82+ try {
83+ const raw = await readFile ( capturePath , 'utf8' ) ;
84+ return raw
85+ . trim ( )
86+ . split ( / \r ? \n / u)
87+ . filter ( Boolean )
88+ . map ( ( line ) => JSON . parse ( line ) ) ;
89+ } catch ( error ) {
90+ if ( error ?. code === 'ENOENT' ) {
91+ return [ ] ;
92+ }
93+ throw error ;
94+ }
95+ }
96+
97+ function assertNoDetailsLeak ( event , forbiddenValues ) {
98+ const serialized = JSON . stringify ( event . details ?? { } ) ;
99+ for ( const value of forbiddenValues ) {
100+ assert . equal (
101+ serialized . includes ( value ) ,
102+ false ,
103+ `governance details leaked forbidden value ${ value } : ${ serialized } `
104+ ) ;
105+ }
106+ }
107+
74108async function readFixtureJson ( section , fileName ) {
75109 return JSON . parse (
76110 await readFile (
@@ -123,6 +157,212 @@ async function writeBlockerFlag(projectRoot, objectiveId) {
123157 } ) ;
124158}
125159
160+ test ( 'objective start emits an objective_started governance event after state commit' , async ( ) => {
161+ const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-governance-start-' ) ;
162+ const capturePath = path . join ( projectRoot , '.governance-events.jsonl' ) ;
163+ try {
164+ const result = await runVre ( projectRoot , buildObjectiveStartArgs ( ) , {
165+ env : {
166+ ...FIXTURE_KERNEL_ENV ,
167+ VRE_SESSION_ID : 'sess-governance-start' ,
168+ VRE_GOVERNANCE_CAPTURE_PATH : capturePath
169+ }
170+ } ) ;
171+ assert . equal ( result . code , 0 , `stderr=${ result . stderr } ` ) ;
172+
173+ const objectiveRecord = await readJson (
174+ path . join ( projectRoot , '.vibe-science-environment' , 'objectives' , 'OBJ-001' , 'objective.json' )
175+ ) ;
176+ assert . equal ( objectiveRecord . status , 'active' ) ;
177+
178+ const events = await readGovernanceEvents ( capturePath ) ;
179+ assert . equal ( events . length , 1 ) ;
180+ assert . equal ( events [ 0 ] . event_type , 'objective_started' ) ;
181+ assert . equal ( events [ 0 ] . source_component , 'vre/objectives/cli' ) ;
182+ assert . equal ( events [ 0 ] . objective_id , 'OBJ-001' ) ;
183+ assert . deepEqual ( events [ 0 ] . details , {
184+ runtimeMode : 'unattended-batch' ,
185+ reasoningMode : 'rule-only'
186+ } ) ;
187+ assertNoDetailsLeak ( events [ 0 ] , [ 'demo objective' , 'why-now' , projectRoot ] ) ;
188+ } finally {
189+ await cleanupCliFixtureProject ( projectRoot ) ;
190+ }
191+ } ) ;
192+
193+ test ( 'objective stop emits an objective_completed governance event with terminal status only' , async ( ) => {
194+ const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-governance-stop-' ) ;
195+ const capturePath = path . join ( projectRoot , '.governance-events.jsonl' ) ;
196+ const secretReason = 'operator closure SECRET-seq116-stop' ;
197+ try {
198+ await runVre ( projectRoot , buildObjectiveStartArgs ( ) , {
199+ env : {
200+ ...FIXTURE_KERNEL_ENV ,
201+ VRE_SESSION_ID : 'sess-governance-stop'
202+ }
203+ } ) ;
204+
205+ const result = await runVre ( projectRoot , [
206+ 'objective' ,
207+ 'stop' ,
208+ '--objective' ,
209+ 'OBJ-001' ,
210+ '--reason' ,
211+ secretReason
212+ ] , {
213+ env : {
214+ ...FIXTURE_KERNEL_ENV ,
215+ VRE_GOVERNANCE_CAPTURE_PATH : capturePath
216+ }
217+ } ) ;
218+ assert . equal ( result . code , 0 , `stderr=${ result . stderr } ` ) ;
219+
220+ const objectiveRecord = await readJson (
221+ path . join ( projectRoot , '.vibe-science-environment' , 'objectives' , 'OBJ-001' , 'objective.json' )
222+ ) ;
223+ assert . equal ( objectiveRecord . status , 'abandoned' ) ;
224+
225+ const events = await readGovernanceEvents ( capturePath ) ;
226+ assert . equal ( events . length , 1 ) ;
227+ assert . equal ( events [ 0 ] . event_type , 'objective_completed' ) ;
228+ assert . equal ( events [ 0 ] . source_component , 'vre/objectives/cli' ) ;
229+ assert . equal ( events [ 0 ] . objective_id , 'OBJ-001' ) ;
230+ assert . deepEqual ( events [ 0 ] . details , {
231+ terminalStatus : 'abandoned'
232+ } ) ;
233+ assertNoDetailsLeak ( events [ 0 ] , [ secretReason , projectRoot , 'active-objective.json' ] ) ;
234+ } finally {
235+ await cleanupCliFixtureProject ( projectRoot ) ;
236+ }
237+ } ) ;
238+
239+ test ( 'objective pause emits an objective_paused governance event without raw operator reason' , async ( ) => {
240+ const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-governance-pause-' ) ;
241+ const capturePath = path . join ( projectRoot , '.governance-events.jsonl' ) ;
242+ const rawReason = 'operator pause SECRET-seq116-pause C:/sensitive/path' ;
243+ try {
244+ await runVre ( projectRoot , buildObjectiveStartArgs ( ) , {
245+ env : {
246+ ...FIXTURE_KERNEL_ENV ,
247+ VRE_SESSION_ID : 'sess-governance-pause'
248+ }
249+ } ) ;
250+
251+ const result = await runVre ( projectRoot , [
252+ 'objective' ,
253+ 'pause' ,
254+ '--objective' ,
255+ 'OBJ-001' ,
256+ '--reason' ,
257+ rawReason
258+ ] , {
259+ env : {
260+ ...FIXTURE_KERNEL_ENV ,
261+ VRE_GOVERNANCE_CAPTURE_PATH : capturePath
262+ }
263+ } ) ;
264+ assert . equal ( result . code , 0 , `stderr=${ result . stderr } ` ) ;
265+
266+ const objectiveRecord = await readJson (
267+ path . join ( projectRoot , '.vibe-science-environment' , 'objectives' , 'OBJ-001' , 'objective.json' )
268+ ) ;
269+ assert . equal ( objectiveRecord . status , 'paused' ) ;
270+
271+ const events = await readGovernanceEvents ( capturePath ) ;
272+ assert . equal ( events . length , 1 ) ;
273+ assert . equal ( events [ 0 ] . event_type , 'objective_paused' ) ;
274+ assert . equal ( events [ 0 ] . source_component , 'vre/objectives/cli' ) ;
275+ assert . equal ( events [ 0 ] . objective_id , 'OBJ-001' ) ;
276+ assert . deepEqual ( events [ 0 ] . details , {
277+ pauseReason : 'operator-pause'
278+ } ) ;
279+ assertNoDetailsLeak ( events [ 0 ] , [ rawReason , 'SECRET-seq116-pause' , 'C:/sensitive/path' ] ) ;
280+ } finally {
281+ await cleanupCliFixtureProject ( projectRoot ) ;
282+ }
283+ } ) ;
284+
285+ test ( 'objective resume emits an objective_resumed governance event with boolean details' , async ( ) => {
286+ const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-governance-resume-' ) ;
287+ const capturePath = path . join ( projectRoot , '.governance-events.jsonl' ) ;
288+ try {
289+ await runVre ( projectRoot , buildObjectiveStartArgs ( ) , {
290+ env : {
291+ ...FIXTURE_KERNEL_ENV ,
292+ VRE_SESSION_ID : 'sess-governance-resume'
293+ }
294+ } ) ;
295+ await runVre ( projectRoot , [
296+ 'objective' ,
297+ 'pause' ,
298+ '--objective' ,
299+ 'OBJ-001' ,
300+ '--reason' ,
301+ 'operator pause before resume'
302+ ] , {
303+ env : FIXTURE_KERNEL_ENV
304+ } ) ;
305+
306+ const result = await runVre ( projectRoot , [
307+ 'objective' ,
308+ 'resume' ,
309+ '--objective' ,
310+ 'OBJ-001'
311+ ] , {
312+ env : {
313+ ...FIXTURE_KERNEL_ENV ,
314+ VRE_GOVERNANCE_CAPTURE_PATH : capturePath
315+ }
316+ } ) ;
317+ assert . equal ( result . code , 0 , `stderr=${ result . stderr } ` ) ;
318+
319+ const objectiveRecord = await readJson (
320+ path . join ( projectRoot , '.vibe-science-environment' , 'objectives' , 'OBJ-001' , 'objective.json' )
321+ ) ;
322+ assert . equal ( objectiveRecord . status , 'active' ) ;
323+
324+ const events = await readGovernanceEvents ( capturePath ) ;
325+ assert . equal ( events . length , 1 ) ;
326+ assert . equal ( events [ 0 ] . event_type , 'objective_resumed' ) ;
327+ assert . equal ( events [ 0 ] . source_component , 'vre/objectives/cli' ) ;
328+ assert . equal ( events [ 0 ] . objective_id , 'OBJ-001' ) ;
329+ assert . deepEqual ( events [ 0 ] . details , {
330+ repairSnapshot : false ,
331+ blockerResolved : false
332+ } ) ;
333+ assertNoDetailsLeak ( events [ 0 ] , [ projectRoot , 'resume-snapshot.json' , 'operator pause before resume' ] ) ;
334+ } finally {
335+ await cleanupCliFixtureProject ( projectRoot ) ;
336+ }
337+ } ) ;
338+
339+ test ( 'objective lifecycle governance bridge failure does not roll back the objective command' , async ( ) => {
340+ const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-governance-fail-soft-' ) ;
341+ const missingCli = path . join ( projectRoot , 'missing-governance-log.js' ) ;
342+ try {
343+ const result = await runVre ( projectRoot , buildObjectiveStartArgs ( ) , {
344+ env : {
345+ ...FIXTURE_KERNEL_ENV ,
346+ VRE_SESSION_ID : 'sess-governance-fail-soft' ,
347+ VIBE_SCIENCE_PLUGIN_CLI : missingCli
348+ }
349+ } ) ;
350+ assert . equal ( result . code , 0 , `stderr=${ result . stderr } ` ) ;
351+ assert . doesNotMatch ( result . stderr , new RegExp ( missingCli . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / gu, '\\$&' ) , 'u' ) ) ;
352+
353+ const payload = JSON . parse ( result . stdout ) ;
354+ assert . equal ( payload . ok , true ) ;
355+ assert . equal ( payload . objectiveId , 'OBJ-001' ) ;
356+
357+ const objectiveRecord = await readJson (
358+ path . join ( projectRoot , '.vibe-science-environment' , 'objectives' , 'OBJ-001' , 'objective.json' )
359+ ) ;
360+ assert . equal ( objectiveRecord . status , 'active' ) ;
361+ } finally {
362+ await cleanupCliFixtureProject ( projectRoot ) ;
363+ }
364+ } ) ;
365+
126366test ( 'objective start rejects unattended-batch without a wake policy and emits structured JSON' , async ( ) => {
127367 const projectRoot = await createCliFixtureProject ( 'vre-objective-cli-no-wake-' ) ;
128368 try {
0 commit comments