| 1 | import assert from 'node:assert/strict'; |
| 2 | import { test } from 'node:test'; |
| 3 | import { mkdtempSync, rmSync, existsSync } from 'node:fs'; |
| 4 | import { tmpdir } from 'node:os'; |
| 5 | import { join, resolve } from 'node:path'; |
| 6 | import { execFile } from 'node:child_process'; |
| 7 | import { promisify } from 'node:util'; |
| 8 | import { startLocal } from './coordination-local.mjs'; |
| 9 | const exec = promisify(execFile); |
| 10 | const binary = resolve(`target/debug/g1t${process.platform === 'win32' ? '.exe' : ''}`); |
| 11 | const built = ['apps/api/build/index.js','services/work/build/index.js',binary].every(existsSync); |
| 12 | // A release check must not pass by skipping: G1T_REQUIRE_LOCAL_WORKERS=1 turns a missing build into a failure. |
| 13 | if (!built && process.env.G1T_REQUIRE_LOCAL_WORKERS === '1') throw new Error('Build API/Work Workers (node scripts/deploy.mjs build --only work,api) and the g1t CLI (cargo build -p g1t) first'); |
| 14 | |
| 15 | test('real Workers: CLI and MCP share durable updates, receipts and ownership rules', { skip: !built && 'Build API/Work Workers and the g1t CLI first', timeout: 60000 }, async () => { |
| 16 | const dir=mkdtempSync(join(tmpdir(),'g1t-local-coordination-')); |
| 17 | let local; |
| 18 | try { |
| 19 | local=await startLocal({port:0,persist:dir}); |
| 20 | const rest=async(path,body,token='local-rehearsal')=>{ |
| 21 | const response=await fetch(`${local.url}/repos/tester/rehearsal/${path}`,{method:body?'POST':'GET',headers:{authorization:`Bearer ${token}`,'content-type':'application/json'},body:body?JSON.stringify(body):undefined}); |
| 22 | const result=await response.json(); assert.equal(response.status,200,JSON.stringify(result)); return result; |
| 23 | }; |
| 24 | const mcp=async(args,token='local-rehearsal')=>{ |
| 25 | const response=await fetch(`${local.url}/mcp`,{method:'POST',headers:{authorization:`Bearer ${token}`,'content-type':'application/json'},body:JSON.stringify({jsonrpc:'2.0',id:1,method:'tools/call',params:{name:'session',arguments:{repo:'tester/rehearsal',...args}}})}); |
| 26 | const text=await response.text(); assert.equal(response.status,200,text); return JSON.parse(text).result; |
| 27 | }; |
| 28 | const unwrap=result=>{assert.equal(result.isError,false,JSON.stringify(result));return JSON.parse(result.content[0].text);}; |
| 29 | const a=await rest('sessions',{harness:'codex',harness_session_id:'codex-test'}); |
| 30 | const b=unwrap(await mcp({action:'start',harness:'claude_code',harness_session_id:'claude-test'})); |
| 31 | await rest(`sessions/${a.id}/entries`,{entries:[{kind:'note',text:'Use fetchUser; implementation belongs to Codex',key:'plan'}],touched:[{kind:'file',value:'src/contract.ts'}]}); |
| 32 | unwrap(await mcp({action:'record',id:b.id,entries:[{kind:'note',text:'Claude owns caller tests',key:'plan'}],touched:[{kind:'file',value:'src/contract.ts'}]})); |
| 33 | let detail=await rest(`sessions/${a.id}`); |
| 34 | const overlap=detail.updates.find(u=>u.kind==='overlap.detected'); assert.ok(overlap); |
| 35 | const bOverlap=unwrap(await mcp({action:'get',id:b.id})).updates.find(u=>u.kind==='overlap.detected'); |
| 36 | assert.ok(bOverlap); assert.notEqual(bOverlap.id,overlap.id); |
| 37 | assert.ok((await rest(`sessions/${a.id}`)).updates.some(u=>u.id===overlap.id),'reading never acknowledges'); |
| 38 | const foreign=await mcp({action:'record',id:a.id,acknowledge:[overlap.id]},'local-other'); |
| 39 | assert.equal(foreign.isError,true,'another actor cannot acknowledge this session'); |
| 40 | // The same person's other session cannot clear it either, and is told so. |
| 41 | const sibling=unwrap(await mcp({action:'record',id:b.id,acknowledge:[overlap.id]})); |
| 42 | assert.equal(sibling.acknowledged,0,'an ID from another session is not counted'); |
| 43 | assert.ok((await rest(`sessions/${a.id}`)).updates.some(u=>u.id===overlap.id),'a sibling session cannot clear this inbox'); |
| 44 | const args=['-R','tester/rehearsal','session','acknowledge',overlap.id,'--session',a.id,'--note','Separate scopes; agree the interface before editing','--json']; |
| 45 | for(let n=0;n<2;n++) await exec(binary,args,{env:{...process.env,G1T_TOKEN:'local-rehearsal',G1T_API_URL:local.url,G1T_SESSION:b.id},windowsHide:true}); |
| 46 | detail=await rest(`sessions/${a.id}`); |
| 47 | assert.ok(!detail.updates.some(u=>u.id===overlap.id)); |
| 48 | assert.equal(detail.entries.filter(e=>e.text.includes('Separate scopes;')).length,1,'a retry records one decision'); |
| 49 | assert.ok(unwrap(await mcp({action:'get',id:b.id})).updates.some(u=>u.id===bOverlap.id),'one harness cannot clear the other inbox'); |
| 50 | await local.mf.dispose(); |
| 51 | local=await startLocal({port:0,persist:dir}); |
| 52 | assert.ok(!(await rest(`sessions/${a.id}`)).updates.some(u=>u.id===overlap.id),'receipt survives restart'); |
| 53 | assert.ok(unwrap(await mcp({action:'get',id:b.id})).updates.some(u=>u.id===bOverlap.id),'pending update survives restart'); |
| 54 | const resumed=await rest('sessions',{harness:'codex',harness_session_id:'codex-test'}); |
| 55 | assert.equal(resumed.id,a.id,'a restarted harness recovers its session'); |
| 56 | assert.equal(unwrap(await mcp({action:'record',id:b.id,entries:[{kind:'note',text:'Handoff considered',key:'ack'}],acknowledge:[bOverlap.id]})).acknowledged,1); |
| 57 | assert.ok(!unwrap(await mcp({action:'get',id:b.id})).updates.some(u=>u.id===bOverlap.id)); |
| 58 | } finally {if(local)await local.mf.dispose();rmSync(dir,{recursive:true,force:true,maxRetries:10,retryDelay:100});} |
| 59 | }); |