mobile-debug-mcp 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/marketplace.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/README.md +86 -24
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +37 -8
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +36 -16
- package/dist/debugger/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/sessions.js +33 -16
- package/dist/debugger/worker.js +6 -4
- package/dist/interact/index.js +12 -2
- package/dist/manage/ios.js +2 -0
- package/dist/observe/ios-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +244 -0
- package/dist/observe/readiness-service.js +1 -1
- package/dist/observe/readiness.js +4 -4
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +4 -8
- package/dist/server/handlers/debugger-handlers.js +27 -8
- package/dist/server/handlers/diagnostic.js +12 -4
- package/dist/server/handlers/tool-handlers.js +19 -9
- package/dist/server/schemas/debugger-schema.js +10 -7
- package/dist/server/schemas/system-trace-schema.js +14 -9
- package/dist/server-core.js +1 -1
- package/dist/server.js +4 -4
- package/dist/utils/ios/utils.js +3 -0
- package/docs/CHANGELOG.md +23 -1
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/code-structure.md +3 -1
- package/docs/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +138 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/tools/TOOLS.md +2 -2
- package/docs/tools/debugger.md +45 -5
- package/docs/tools/system-traces.md +81 -14
- package/package.json +2 -1
- package/scripts/agent-plugin.mjs +55 -5
- package/src/debugger/android.ts +1 -1
- package/src/debugger/guard.ts +5 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/sessions.ts +24 -12
- package/src/debugger/worker.ts +7 -4
- package/src/interact/index.ts +13 -2
- package/src/manage/ios.ts +2 -0
- package/src/observe/ios-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +204 -0
- package/src/observe/readiness-service.ts +1 -1
- package/src/observe/readiness.ts +4 -4
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +4 -4
- package/src/server/handlers/debugger-handlers.ts +28 -9
- package/src/server/handlers/diagnostic.ts +11 -4
- package/src/server/handlers/tool-handlers.ts +20 -9
- package/src/server/schemas/debugger-schema.ts +10 -7
- package/src/server/schemas/system-trace-schema.ts +15 -9
- package/src/server-core.ts +1 -1
- package/src/server.ts +3 -3
- package/src/utils/ios/utils.ts +3 -0
- package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/fixtures/contracts/tool-definitions.json +192 -32
- package/test/fixtures/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/handle-guards.test.ts +88 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -0
- package/test/unit/observe/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- package/test/unit/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -0
- package/test/unit/server/system-trace.test.ts +12 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { IosTraceSessions } from '../../../src/observe/ios-trace/sessions.js'
|
|
5
|
+
import { IosTraceNative, type IosRecording } from '../../../src/observe/ios-trace/native.js'
|
|
6
|
+
import type { TraceClock } from '../../../src/observe/system-trace.js'
|
|
7
|
+
import type { TraceCommandResult } from '../../../src/observe/ios-trace/process.js'
|
|
8
|
+
|
|
9
|
+
const target = { platform: 'ios' as const, id: 'sim', simulator: true, osVersion: '26.4', model: 'fixture' }
|
|
10
|
+
function fixture() {
|
|
11
|
+
let monitorTick = 0, exitOnMonitor = false, exportMessage = 'EXPORT_FAILED'
|
|
12
|
+
let now = 1000, failStart = false, failStop = false, failExport = false, monitor = '', stops = 0, starts = 0, validations = 0
|
|
13
|
+
const timers = new Map<ReturnType<typeof setTimeout>, () => void>(), recordings: IosRecording[] = [], directories: string[] = []
|
|
14
|
+
const time: TraceClock = { wall: () => now, mono: () => now, sleep: async ms => {now += ms},
|
|
15
|
+
schedule: fn => { const id = {} as ReturnType<typeof setTimeout>; timers.set(id,fn); return id }, clear: id => {timers.delete(id)} }
|
|
16
|
+
class Native extends IosTraceNative {
|
|
17
|
+
resolve = async () => ({ app_id:'fixture', pid:100, executable:'/fixture.app/Fixture',started_at:'fixture-start' })
|
|
18
|
+
async start(_d: string, _app: unknown, directory: string, _duration: number, own: (r: IosRecording) => void) {
|
|
19
|
+
starts++; directories.push(directory)
|
|
20
|
+
let alive = true, resolve!: (v: TraceCommandResult) => void
|
|
21
|
+
const done = new Promise<TraceCommandResult>(r => {resolve=r})
|
|
22
|
+
const recording: IosRecording = {file:path.join(directory,'capture.trace'), ready_at_ms:now,
|
|
23
|
+
recorder:{done, alive:()=>alive, line:async()=>{},signal:()=>{alive=false;resolve({code:0,output:'Reached specified time limit',timedOut:false})}}}
|
|
24
|
+
own(recording); recordings.push(recording)
|
|
25
|
+
await fs.mkdir(recording.file)
|
|
26
|
+
await fs.writeFile(path.join(recording.file,'data'),'sample')
|
|
27
|
+
if(failStart) throw new Error('READINESS_TIMEOUT')
|
|
28
|
+
return recording
|
|
29
|
+
}
|
|
30
|
+
async stop(r: IosRecording) {stops++; if(failStop) throw new Error('ACTION_TIMEOUT');r.recorder.signal('SIGINT');return r.recorder.done}
|
|
31
|
+
async monitor(device: string, app: {pid:number;executable:string}, recording: IosRecording) {
|
|
32
|
+
assert.equal(device,target.id);assert.equal(app.pid,100);assert.equal(app.executable,'/fixture.app/Fixture')
|
|
33
|
+
assert.equal(recording,recordings.at(-1),'Retries must retain the same owned recording')
|
|
34
|
+
now+=monitorTick
|
|
35
|
+
if(exitOnMonitor)recording.recorder.signal('SIGINT')
|
|
36
|
+
if(monitor)throw new Error(monitor)
|
|
37
|
+
}
|
|
38
|
+
async validate(r: IosRecording) {validations++;if(failExport)throw new Error(exportMessage);return {path:r.file,format:'instruments' as const,kind:'directory' as const,size_bytes:6,validation:{readable:true,schema:'time-sample',sample_rows:1}}}
|
|
39
|
+
shutdown() {for(const r of recordings)r.recorder.signal('SIGINT')}
|
|
40
|
+
}
|
|
41
|
+
const sessions = new IosTraceSessions(new Native(),time)
|
|
42
|
+
return {sessions,recordings,timers,flags:(v:{start?:boolean;stop?:boolean;export?:boolean;monitor?:string;monitorTick?:number;exitOnMonitor?:boolean;exportMessage?:string})=>{failStart=v.start??false;failStop=v.stop??false;failExport=v.export??false;monitor=v.monitor??'';monitorTick=v.monitorTick??0;exitOnMonitor=v.exitOnMonitor??false;exportMessage=v.exportMessage??'EXPORT_FAILED'},
|
|
43
|
+
tick:(ms:number)=>{now+=ms},counts:()=>({stops,starts,validations}),
|
|
44
|
+
cleanup:async()=>{await sessions.shutdown();for(const d of directories)await fs.rm(d,{recursive:true,force:true})}}
|
|
45
|
+
}
|
|
46
|
+
async function run() {
|
|
47
|
+
const f=fixture()
|
|
48
|
+
try {
|
|
49
|
+
const start=await f.sessions.start(target,'fixture')
|
|
50
|
+
await assert.rejects(()=>f.sessions.start(target,'fixture'),/TRACE_BUSY/)
|
|
51
|
+
assert.equal(start.state,'recording');assert.equal(start.recording_ready_at_ms,1000)
|
|
52
|
+
const finish=await f.sessions.link(start.trace_id,target,'journey')
|
|
53
|
+
for(const device of [{platform:'android',id:'sim'},{platform:'ios',id:'other'}])await assert.rejects(()=>f.sessions.link(start.trace_id,device,'wrong'),/TRACE_NOT_FOUND/)
|
|
54
|
+
const [a,b]=await Promise.all([f.sessions.control(start.trace_id,'stop'),f.sessions.control(start.trace_id,'stop')])
|
|
55
|
+
assert.equal(a.state,'completed');assert.deepEqual(a,b);assert.deepEqual(a.correlation_ids,['journey']);assert.equal((await finish()).coverage,'partial')
|
|
56
|
+
assert.equal(f.counts().validations,1)
|
|
57
|
+
await assert.rejects(()=>f.sessions.link(start.trace_id,target,'stale'),/TRACE_NOT_FOUND/)
|
|
58
|
+
await assert.rejects(()=>f.sessions.control('foreign','stop'),/TRACE_NOT_FOUND/)
|
|
59
|
+
assert.equal((await f.sessions.control(start.trace_id,'cancel')).artifact?.path,a.artifact?.path)
|
|
60
|
+
const cancelled=await f.sessions.start(target,'fixture')
|
|
61
|
+
const dir=path.dirname(f.recordings[1].file)
|
|
62
|
+
assert.equal((await f.sessions.control(cancelled.trace_id,'cancel')).state,'cancelled')
|
|
63
|
+
await assert.rejects(()=>fs.stat(dir),{code:'ENOENT'})
|
|
64
|
+
const expires=await f.sessions.start(target,'fixture',1000);f.tick(1001)
|
|
65
|
+
const expired=await f.sessions.control(expires.trace_id,'status')
|
|
66
|
+
assert.equal(expired.state,'completed');assert.equal(expired.completion_reason,'duration_limit')
|
|
67
|
+
f.tick(600001);await assert.rejects(()=>f.sessions.control(start.trace_id,'status'),/TRACE_NOT_FOUND/)
|
|
68
|
+
}finally{await f.cleanup()}
|
|
69
|
+
for(const failure of ['start','stop','export','monitor'] as const){
|
|
70
|
+
const f=fixture()
|
|
71
|
+
try{
|
|
72
|
+
if(failure==='start')f.flags({start:true})
|
|
73
|
+
const s=await f.sessions.start(target,'fixture')
|
|
74
|
+
if(failure==='start'){assert.equal(s.state,'failed');assert.equal(s.reason,'READINESS_TIMEOUT');assert.equal(s.cleanup.status,'complete');assert.equal(f.recordings[0].recorder.alive(),false);continue}
|
|
75
|
+
f.flags(failure==='monitor'?{monitor:'STORAGE_LIMIT'}:{[failure]:true})
|
|
76
|
+
const failed=await f.sessions.control(s.trace_id,failure==='monitor'?'status':'stop')
|
|
77
|
+
assert.equal(failed.state,'failed');assert.equal(failed.artifact,null)
|
|
78
|
+
if(failure==='monitor'){assert.equal(failed.reason,'STORAGE_LIMIT');assert.equal(failed.cleanup.status,'complete');continue}
|
|
79
|
+
f.flags({})
|
|
80
|
+
assert.equal((await f.sessions.control(s.trace_id,'stop')).state,'completed')
|
|
81
|
+
assert.equal(f.counts().starts,1,'Retry must not start a new recording')
|
|
82
|
+
}finally{await f.cleanup()}
|
|
83
|
+
}
|
|
84
|
+
const exportClassification=fixture()
|
|
85
|
+
try {
|
|
86
|
+
const s=await exportClassification.sessions.start(target,'fixture')
|
|
87
|
+
exportClassification.flags({export:true,exportMessage:'COMMAND_FAILED'})
|
|
88
|
+
const failed=await exportClassification.sessions.control(s.trace_id,'stop')
|
|
89
|
+
assert.equal(failed.reason,'TRACE_ARTIFACT_FAILED','Monitor classification must not alter export errors')
|
|
90
|
+
assert.match(failed.recovery!,/Retry stop/)
|
|
91
|
+
}finally{await exportClassification.cleanup()}
|
|
92
|
+
for(const reason of ['COMMAND_FAILED','ACTION_TIMEOUT','TARGET_DISCONNECTED','private unexpected output']) {
|
|
93
|
+
const f=fixture()
|
|
94
|
+
try {
|
|
95
|
+
const s=await f.sessions.start(target,'fixture')
|
|
96
|
+
f.flags({monitor:reason})
|
|
97
|
+
for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
|
|
98
|
+
assert.equal(f.counts().stops,0)
|
|
99
|
+
f.flags({});assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
|
|
100
|
+
f.flags({monitor:reason})
|
|
101
|
+
for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording','Successful poll must reset consecutive failure count')
|
|
102
|
+
const failed=await f.sessions.control(s.trace_id,'status')
|
|
103
|
+
assert.equal(failed.state,'failed');assert.equal(failed.reason,reason==='private unexpected output'?'TRACE_MONITOR_FAILED':reason)
|
|
104
|
+
assert.equal(failed.cleanup.status,'complete');assert.equal(failed.artifact,null)
|
|
105
|
+
assert.equal(f.counts().starts,1);assert.equal(f.recordings[0].recorder.alive(),false)
|
|
106
|
+
assert.ok(!JSON.stringify(failed).includes('private unexpected output'))
|
|
107
|
+
}finally{await f.cleanup()}
|
|
108
|
+
}
|
|
109
|
+
for(const reason of ['APP_IDENTITY_CHANGED','APP_NOT_RUNNING']) {
|
|
110
|
+
const f=fixture()
|
|
111
|
+
try {
|
|
112
|
+
const s=await f.sessions.start(target,'fixture');f.flags({monitor:reason})
|
|
113
|
+
const failed=await f.sessions.control(s.trace_id,'status')
|
|
114
|
+
assert.equal(failed.state,'failed');assert.equal(failed.reason,reason);assert.equal(failed.cleanup.status,'complete')
|
|
115
|
+
}finally{await f.cleanup()}
|
|
116
|
+
}
|
|
117
|
+
for(const ended of ['deadline','recorder'] as const) {
|
|
118
|
+
const f=fixture()
|
|
119
|
+
try {
|
|
120
|
+
const s=await f.sessions.start(target,'fixture',1000)
|
|
121
|
+
f.flags({monitor:'COMMAND_FAILED',monitorTick:ended==='deadline'?1001:0,exitOnMonitor:ended==='recorder'})
|
|
122
|
+
const result=await f.sessions.control(s.trace_id,'status')
|
|
123
|
+
assert.equal(result.state,'completed','A tolerated failed poll must still reconcile duration/recorder completion')
|
|
124
|
+
assert.equal(result.completion_reason,'duration_limit');assert.ok(result.artifact)
|
|
125
|
+
}finally{await f.cleanup()}
|
|
126
|
+
}
|
|
127
|
+
const shutdown=fixture()
|
|
128
|
+
try{const s=await shutdown.sessions.start(target,'fixture');assert.deepEqual(await shutdown.sessions.shutdown(),[]);assert.equal((await shutdown.sessions.control(s.trace_id,'status')).state,'cancelled');await assert.rejects(()=>shutdown.sessions.start(target,'fixture'),/TRACE_BUSY/)}finally{await shutdown.cleanup()}
|
|
129
|
+
console.log('PASS iOS trace lifecycle, linkage, failure cleanup, retries and expiry')
|
|
130
|
+
}
|
|
131
|
+
run().catch(error=>{console.error(error);process.exitCode=1})
|
|
@@ -36,6 +36,6 @@ assert.equal(tracker.summary.capabilities.debugger_paused, true)
|
|
|
36
36
|
assert.ok(tracker.deadline().limitations.includes('DEADLINE_EXCEEDED'))
|
|
37
37
|
assert.equal(new ReadinessTracker('android', 'fixture').observe(sample(), 10000, 9000).state, 'unknown', 'no cross-request history')
|
|
38
38
|
assert.ok(!JSON.stringify(tracker.summary).includes('Ready'), 'evidence contains codes rather than private labels')
|
|
39
|
-
assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused,
|
|
39
|
+
assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused, true)
|
|
40
40
|
assert.ok(tracker.summary.limitations.includes('MANAGED_DEBUGGER_ONLY'))
|
|
41
41
|
console.log('Readiness classifier: PASS')
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { SystemTraceRouter } from '../../../src/observe/system-trace-router.js'
|
|
3
|
+
import type { SystemTraceSessions, TraceResult } from '../../../src/observe/system-trace.js'
|
|
4
|
+
import type { IosTraceSessions, IosTraceResult } from '../../../src/observe/ios-trace/sessions.js'
|
|
5
|
+
import type { IosTraceNative } from '../../../src/observe/ios-trace/native.js'
|
|
6
|
+
function backend(platform:'android'|'ios') {
|
|
7
|
+
let next=0,starts=0
|
|
8
|
+
const results=new Map<string,TraceResult|IosTraceResult>()
|
|
9
|
+
const api={
|
|
10
|
+
capabilities:async()=>({}),
|
|
11
|
+
start:async(device:string|{id:string})=>{
|
|
12
|
+
starts++
|
|
13
|
+
const result={trace_id:platform+ ++next,device:{platform,id:typeof device==='string'?device:device.id},state:'recording',cleanup:{status:'pending'}} as TraceResult|IosTraceResult
|
|
14
|
+
results.set(result.trace_id,result);return result
|
|
15
|
+
},
|
|
16
|
+
control:async(id:string,op:string)=>{
|
|
17
|
+
const result=results.get(id);if(!result)throw new Error('TRACE_NOT_FOUND')
|
|
18
|
+
if(op==='cancel'){result.cleanup.status='complete';result.state='cancelled'}
|
|
19
|
+
return result
|
|
20
|
+
},
|
|
21
|
+
link:async()=>async()=>({trace_id:'linked',coverage:'covered',clock_alignment:'unknown'}),
|
|
22
|
+
shutdown:async()=>[]
|
|
23
|
+
}
|
|
24
|
+
return {api,results,count:()=>starts}
|
|
25
|
+
}
|
|
26
|
+
async function run(){
|
|
27
|
+
const android=backend('android'),ios=backend('ios')
|
|
28
|
+
let release!:()=>void,gate=Promise.resolve()
|
|
29
|
+
const native={shutdown:()=>{},capabilities:async(device:string)=>{await gate;return {supported:true,device:{platform:'ios',id:device,simulator:true}}}} as unknown as IosTraceNative
|
|
30
|
+
const router=new SystemTraceRouter(android.api as unknown as SystemTraceSessions,ios.api as unknown as IosTraceSessions,native)
|
|
31
|
+
const a=await router.start('a'),b=await router.start('b',30000,{appId:'fixture'})
|
|
32
|
+
await assert.rejects(()=>router.link(b.trace_id,{platform:'android',id:'b'},'bad'),/TRACE_NOT_FOUND/)
|
|
33
|
+
await assert.rejects(()=>router.link(b.trace_id,{platform:'ios',id:'other'},'bad'),/TRACE_NOT_FOUND/)
|
|
34
|
+
await assert.rejects(()=>router.start('b',30000,{appId:'fixture'}),/TRACE_BUSY/)
|
|
35
|
+
await router.start('c');await router.start('d',30000,{appId:'fixture'})
|
|
36
|
+
await assert.rejects(()=>router.start('fifth'),/TRACE_BUSY/)
|
|
37
|
+
await router.control(a.trace_id,'cancel')
|
|
38
|
+
await router.start('freed')
|
|
39
|
+
await router.control(b.trace_id,'cancel')
|
|
40
|
+
gate=new Promise<void>(r=>{release=r})
|
|
41
|
+
const first=router.start('pending',30000,{appId:'fixture'})
|
|
42
|
+
await new Promise(r=>setImmediate(r))
|
|
43
|
+
await assert.rejects(()=>router.start('pending',30000,{appId:'fixture'}),/TRACE_BUSY/)
|
|
44
|
+
release();await first
|
|
45
|
+
assert.equal(ios.count(),3)
|
|
46
|
+
await router.shutdown();await assert.rejects(()=>router.start('closed'),/TRACE_BUSY/)
|
|
47
|
+
console.log('PASS cross-platform trace routing, pinned linkage, shared reservations and shutdown')
|
|
48
|
+
}
|
|
49
|
+
run().catch(error=>{console.error(error);process.exitCode=1})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const root = fileURLToPath(new URL('../../../', import.meta.url))
|
|
9
|
+
const temporary = await mkdtemp(path.join(os.tmpdir(), 'agent-plugin-export-'))
|
|
10
|
+
const output = path.join(temporary, 'plugin')
|
|
11
|
+
const exportPlugin = () => spawnSync(process.execPath, ['scripts/agent-plugin.mjs', '--export-antigravity', output], { cwd: root, encoding: 'utf8', timeout: 10000 })
|
|
12
|
+
try {
|
|
13
|
+
const exported = exportPlugin()
|
|
14
|
+
assert.equal(exported.status, 0, exported.stderr)
|
|
15
|
+
assert.deepEqual((await readdir(output)).sort(), ['LICENSE', 'README.md', 'assets', 'mcp_config.json', 'plugin.json', 'skills'])
|
|
16
|
+
for (const directory of ['skills', 'assets']) {
|
|
17
|
+
const files = await readdir(path.join(output, directory), { recursive: true })
|
|
18
|
+
assert.ok(files.every(file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._')), 'Export must exclude Finder metadata')
|
|
19
|
+
}
|
|
20
|
+
const manifest = JSON.parse(await readFile(path.join(output, 'plugin.json'), 'utf8'))
|
|
21
|
+
assert.equal(manifest.$schema, 'https://antigravity.google/schemas/v1/plugin.json')
|
|
22
|
+
assert.ok((await readFile(path.join(output, manifest.logo))).length > 0)
|
|
23
|
+
const config = JSON.parse(await readFile(path.join(output, 'mcp_config.json'), 'utf8'))
|
|
24
|
+
assert.equal(Object.keys(config.mcpServers).length, 1)
|
|
25
|
+
const server = config.mcpServers['mobile-debug-mcp']
|
|
26
|
+
assert.equal(server.command, 'npx')
|
|
27
|
+
assert.equal(server.cwd, '${PLUGIN_DATA}')
|
|
28
|
+
assert.equal('type' in server, false, 'Antigravity infers stdio transport; type causes a strict warning')
|
|
29
|
+
const skill = 'skills/mobile-debug-mcp/SKILL.md'
|
|
30
|
+
assert.deepEqual(await readFile(path.join(output, skill)), await readFile(path.join(root, 'agent-plugin', skill)))
|
|
31
|
+
assert.deepEqual(await readFile(path.join(output, 'LICENSE')), await readFile(path.join(root, 'LICENSE')))
|
|
32
|
+
await writeFile(path.join(output, 'keep.txt'), 'existing user data')
|
|
33
|
+
assert.notEqual(exportPlugin().status, 0, 'Existing destinations must not be overwritten or merged')
|
|
34
|
+
assert.equal(await readFile(path.join(output, 'keep.txt'), 'utf8'), 'existing user data')
|
|
35
|
+
console.log('PASS Antigravity export: native inventory, shared skill/license, no foreign manifests, overwrite refusal')
|
|
36
|
+
} finally {
|
|
37
|
+
await rm(temporary, { recursive: true, force: true })
|
|
38
|
+
}
|
|
@@ -8,6 +8,7 @@ import { withEvidence, bindSystemTrace, bindEvidenceDevice } from '../../../src/
|
|
|
8
8
|
const parse = (r: any) => JSON.parse(r.content[0].text)
|
|
9
9
|
async function run() {
|
|
10
10
|
const originalLink = systemTraces.link, originalStart = systemTraces.start, originalControl = systemTraces.control
|
|
11
|
+
const originalCapabilities = systemTraces.iosCapabilities
|
|
11
12
|
const originalApp = AndroidManage.prototype.startApp
|
|
12
13
|
const id = '62a51510-c303-4a1b-af0e-f97cc976d740'
|
|
13
14
|
let dispatched = 0, starts = 0
|
|
@@ -24,8 +25,16 @@ async function run() {
|
|
|
24
25
|
}
|
|
25
26
|
systemTraces.start = async () => { starts++; throw new Error('must not run') }
|
|
26
27
|
const ios = await handleToolCall('start_system_trace', { platform: 'ios' })
|
|
27
|
-
assert.equal(parse(ios).error.code, '
|
|
28
|
-
assert.equal(parse(await handleToolCall('get_system_trace_capabilities', { platform: 'ios' })).
|
|
28
|
+
assert.equal(parse(ios).error.code, 'INVALID_ARGUMENT'); assert.equal(starts, 0)
|
|
29
|
+
assert.equal(parse(await handleToolCall('get_system_trace_capabilities', { platform: 'ios' })).error.code, 'INVALID_ARGUMENT')
|
|
30
|
+
for (const args of [{platform:'ios',deviceId:'sim'}, {platform:'android',appId:'fixture'}, {platform:'android',template:'Time Profiler'}, {platform:'ios',deviceId:'sim',appId:'fixture',template:'Other'}, {platform:'ios',deviceId:'sim',appId:'invalid bundle'}]) {
|
|
31
|
+
assert.equal(parse(await handleToolCall('start_system_trace', args)).error.code, 'INVALID_ARGUMENT')
|
|
32
|
+
}
|
|
33
|
+
systemTraces.iosCapabilities = async (deviceId, appId, template) => {
|
|
34
|
+
assert.equal(deviceId, 'sim'); assert.equal(appId, 'fixture'); assert.equal(template, 'Time Profiler')
|
|
35
|
+
return {supported: false, backend:'xctrace'} as any
|
|
36
|
+
}
|
|
37
|
+
assert.equal(parse(await handleToolCall('get_system_trace_capabilities', {platform:'ios',deviceId:'sim',appId:'fixture',template:'Time Profiler'})).backend, 'xctrace')
|
|
29
38
|
for (const name of ['stop_system_trace', 'cancel_system_trace', 'get_system_trace_status']) {
|
|
30
39
|
assert.equal(parse(await handleToolCall(name, { traceId: id, deviceId: 'wrong' })).error.code, 'INVALID_ARGUMENT')
|
|
31
40
|
assert.equal(parse(await handleToolCall(name, { traceId: id })).error.code, 'TRACE_NOT_FOUND')
|
|
@@ -57,7 +66,7 @@ async function run() {
|
|
|
57
66
|
assert.equal(independent.evidence.trace_context.trace_id, null)
|
|
58
67
|
console.log('system trace tool contracts and journey evidence: PASS')
|
|
59
68
|
} finally {
|
|
60
|
-
systemTraces.link = originalLink; systemTraces.start = originalStart; systemTraces.control = originalControl
|
|
69
|
+
systemTraces.iosCapabilities = originalCapabilities; systemTraces.link = originalLink; systemTraces.start = originalStart; systemTraces.control = originalControl
|
|
61
70
|
AndroidManage.prototype.startApp = originalApp; _resetDeviceListersForTests()
|
|
62
71
|
}
|
|
63
72
|
}
|