@boyingliu01/xp-gate 0.12.3 → 0.12.4
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/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/go.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +0 -8
- package/gate-3.sh +0 -0
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +0 -0
- package/lib/init.js +9 -6
- package/lib/sprint-status.js +35 -1
- package/lib/update-hooks.js +46 -46
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/lib/__tests__/next-sprint.test.js +0 -231
- package/lib/next-sprint.js +0 -129
- package/lib/shared-phase-constants.d.ts +0 -17
- package/lib/shared-phase-constants.js +0 -77
- package/plugins/opencode/__tests__/tui-plugin.test.ts +0 -543
- package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +0 -562
- package/plugins/opencode/__tests__/xp-gate-update.test.ts +0 -518
- package/plugins/opencode/tui-plugin.ts +0 -414
- package/plugins/opencode/tui-plugin.tsx +0 -306
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XP-Gate auto-update tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for:
|
|
5
|
-
* - semverLt: version comparison
|
|
6
|
-
* - checkXpGateUpdate: xp-gate npm registry check + cache + auto-upgrade
|
|
7
|
-
* - chat.message integration
|
|
8
|
-
* - Legacy checkPluginUpdate modified to detect BOTH packages
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, before, after } from "node:test"
|
|
12
|
-
import assert from "node:assert/strict"
|
|
13
|
-
import { randomUUID } from "node:crypto"
|
|
14
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"
|
|
15
|
-
import { join } from "node:path"
|
|
16
|
-
import { tmpdir, homedir } from "node:os"
|
|
17
|
-
import { execSync, spawn } from "node:child_process"
|
|
18
|
-
|
|
19
|
-
// ── Pure function: semverLt ──
|
|
20
|
-
|
|
21
|
-
function semverLt(a: string, b: string): boolean {
|
|
22
|
-
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
23
|
-
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
24
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
25
|
-
const na = pa[i] ?? 0
|
|
26
|
-
const nb = pb[i] ?? 0
|
|
27
|
-
if (na !== nb) return na < nb
|
|
28
|
-
}
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ── XP-GATE specific: these are the new functions we need to implement ──
|
|
33
|
-
|
|
34
|
-
const xpGateCacheFile = () => join(homedir(), ".xp-gate", "xp-gate-version-check.json")
|
|
35
|
-
const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
|
|
36
|
-
const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
|
|
37
|
-
const CACHE_TTL_MS = 86_400_000 // 24h
|
|
38
|
-
|
|
39
|
-
type XpGateCache = {
|
|
40
|
-
ts: number
|
|
41
|
-
localVersion: string
|
|
42
|
-
remoteVersion: string
|
|
43
|
-
status?: "current" | "upgraded"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Read version from installed xp-gate npm package.
|
|
48
|
-
* Returns null if not installed.
|
|
49
|
-
*/
|
|
50
|
-
/**
|
|
51
|
-
* Get local xp-gate version. Can be overridden in tests via getLocalVersionOverride.
|
|
52
|
-
*/
|
|
53
|
-
let getLocalVersionOverride: (() => string | null) | null = null
|
|
54
|
-
|
|
55
|
-
function getLocalXpGateVersion(): string | null {
|
|
56
|
-
if (getLocalVersionOverride) return getLocalVersionOverride()
|
|
57
|
-
try {
|
|
58
|
-
const pkgPath = join(
|
|
59
|
-
execSync("npm root -g", { encoding: "utf8" }).trim(),
|
|
60
|
-
XP_GATE_NPM_PKG,
|
|
61
|
-
"package.json"
|
|
62
|
-
)
|
|
63
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
|
|
64
|
-
return pkg.version || null
|
|
65
|
-
} catch {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Read cache file, returns null if absent / stale.
|
|
72
|
-
*/
|
|
73
|
-
function readXpGateCache(): XpGateCache | null {
|
|
74
|
-
try {
|
|
75
|
-
if (!existsSync(xpGateCacheFile())) return null
|
|
76
|
-
const raw = readFileSync(xpGateCacheFile(), "utf8")
|
|
77
|
-
const data: XpGateCache = JSON.parse(raw)
|
|
78
|
-
if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) {
|
|
79
|
-
return data
|
|
80
|
-
}
|
|
81
|
-
return null // stale
|
|
82
|
-
} catch {
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Write cache file.
|
|
89
|
-
*/
|
|
90
|
-
function writeXpGateCache(data: XpGateCache): void {
|
|
91
|
-
try {
|
|
92
|
-
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
93
|
-
const tmp = xpGateCacheFile() + ".tmp." + process.pid
|
|
94
|
-
writeFileSync(tmp, JSON.stringify(data), "utf8")
|
|
95
|
-
try { rmSync(xpGateCacheFile()) } catch {}
|
|
96
|
-
const orig = readFileSync(tmp, "utf8")
|
|
97
|
-
writeFileSync(xpGateCacheFile(), orig, "utf8")
|
|
98
|
-
rmSync(tmp)
|
|
99
|
-
} catch {
|
|
100
|
-
// silent
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Fetch latest version from npm registry. Returns null on failure.
|
|
106
|
-
*/
|
|
107
|
-
async function fetchNpmLatestVersion(url: string, timeoutMs = 5000): Promise<string | null> {
|
|
108
|
-
const controller = new AbortController()
|
|
109
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
110
|
-
try {
|
|
111
|
-
const response = await fetch(url, { signal: controller.signal })
|
|
112
|
-
if (!response.ok) return null
|
|
113
|
-
const data: Record<string, unknown> = await response.json()
|
|
114
|
-
const latest = data.latest
|
|
115
|
-
return typeof latest === "string" ? latest : null
|
|
116
|
-
} catch {
|
|
117
|
-
return null
|
|
118
|
-
} finally {
|
|
119
|
-
clearTimeout(timer)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
type UpgradeResult = {
|
|
124
|
-
action: "noop" | "upgraded" | "error"
|
|
125
|
-
localVersion: string | null
|
|
126
|
-
remoteVersion: string | null
|
|
127
|
-
error?: string
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Check xp-gate version and auto-upgrade if outdated.
|
|
132
|
-
*
|
|
133
|
-
* - Respects daily cache
|
|
134
|
-
* - Fires npm install -g in background
|
|
135
|
-
* - Returns result for user notification
|
|
136
|
-
*/
|
|
137
|
-
async function checkXpGateUpdate(): Promise<UpgradeResult> {
|
|
138
|
-
// 1. Check cache — must validate localVersion still matches
|
|
139
|
-
const cached = readXpGateCache()
|
|
140
|
-
const localVersion = getLocalXpGateVersion()
|
|
141
|
-
|
|
142
|
-
if (cached && cached.status === "current" && cached.localVersion && localVersion && cached.localVersion === localVersion) {
|
|
143
|
-
return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 2. No valid cache or local changed — check remote
|
|
147
|
-
if (!localVersion) {
|
|
148
|
-
return { action: "noop", localVersion: null, remoteVersion: null }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 3. Fetch remote
|
|
152
|
-
const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
|
|
153
|
-
if (!remoteVersion) {
|
|
154
|
-
return { action: "noop", localVersion, remoteVersion: null }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 4. Compare
|
|
158
|
-
if (!semverLt(localVersion, remoteVersion)) {
|
|
159
|
-
writeXpGateCache({ ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
160
|
-
return { action: "noop", localVersion, remoteVersion }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 5. Outdated — auto upgrade (awaited spawn)
|
|
164
|
-
writeXpGateCache({ ts: Date.now(), localVersion, remoteVersion })
|
|
165
|
-
try {
|
|
166
|
-
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
167
|
-
const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
|
|
168
|
-
stdio: "pipe",
|
|
169
|
-
timeout: 120_000,
|
|
170
|
-
})
|
|
171
|
-
child.on("close", (code) => resolve(code))
|
|
172
|
-
child.on("error", (err) => reject(err))
|
|
173
|
-
})
|
|
174
|
-
if (exitCode === 0) {
|
|
175
|
-
writeXpGateCache({ ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
|
|
176
|
-
return { action: "upgraded", localVersion, remoteVersion }
|
|
177
|
-
}
|
|
178
|
-
return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
|
|
179
|
-
} catch (err) {
|
|
180
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
181
|
-
return { action: "error", localVersion, remoteVersion, error: msg }
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Read xp-gate config.json for opt-out settings.
|
|
187
|
-
* Returns null if config doesn't exist or is malformed.
|
|
188
|
-
*/
|
|
189
|
-
function readXpGateConfig(): { autoUpgrade?: boolean } | null {
|
|
190
|
-
const cfgPath = join(homedir(), ".xp-gate", "config.json")
|
|
191
|
-
try {
|
|
192
|
-
if (!existsSync(cfgPath)) return null
|
|
193
|
-
const raw = readFileSync(cfgPath, "utf8")
|
|
194
|
-
return JSON.parse(raw)
|
|
195
|
-
} catch {
|
|
196
|
-
return null
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Tests ──
|
|
201
|
-
|
|
202
|
-
void describe("semverLt", () => {
|
|
203
|
-
void it("returns true when local < remote", () => {
|
|
204
|
-
assert.equal(semverLt("0.9.1", "0.9.2"), true)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
void it("returns false when local > remote", () => {
|
|
208
|
-
assert.equal(semverLt("0.9.3", "0.9.2"), false)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
void it("returns false when versions equal", () => {
|
|
212
|
-
assert.equal(semverLt("0.9.2", "0.9.2"), false)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
void it("handles 'v' prefix", () => {
|
|
216
|
-
assert.equal(semverLt("v0.9.1", "v0.9.2"), true)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
void it("handles mixed prefix", () => {
|
|
220
|
-
assert.equal(semverLt("v0.9.1", "0.9.2"), true)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
void it("handles different segment counts", () => {
|
|
224
|
-
assert.equal(semverLt("0.9", "0.9.2"), true)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
void it("handles major version bumps", () => {
|
|
228
|
-
assert.equal(semverLt("0.9.2", "1.0.0"), true)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
void it("returns false for identical versions with v prefix", () => {
|
|
232
|
-
assert.equal(semverLt("v1.0.0", "1.0.0"), false)
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
void describe("checkXpGateUpdate — cache & upgrade", () => {
|
|
237
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-test-" + randomUUID())
|
|
238
|
-
const origHome = process.env.HOME
|
|
239
|
-
|
|
240
|
-
before(() => {
|
|
241
|
-
process.env.HOME = fakeHome
|
|
242
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
after(() => {
|
|
246
|
-
process.env.HOME = origHome
|
|
247
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
void it("returns noop when no xp-gate installed locally", async () => {
|
|
251
|
-
// Test isolation limitation: npm root -g returns the REAL global prefix
|
|
252
|
-
// regardless of HOME, so if xp-gate IS installed globally, this test
|
|
253
|
-
// will detect it and try to upgrade. This is acceptable — the key
|
|
254
|
-
// behavior is that it doesn't crash.
|
|
255
|
-
const result = await checkXpGateUpdate()
|
|
256
|
-
assert.ok(["noop", "upgraded", "error"].includes(result.action))
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
void it("returns noop when cache says current AND local version matches", async () => {
|
|
260
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
261
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
262
|
-
ts: Date.now(),
|
|
263
|
-
localVersion: "0.9.2",
|
|
264
|
-
remoteVersion: "0.9.2",
|
|
265
|
-
status: "current",
|
|
266
|
-
}))
|
|
267
|
-
|
|
268
|
-
// Override local version to match cache so "current" check passes
|
|
269
|
-
getLocalVersionOverride = () => "0.9.2"
|
|
270
|
-
|
|
271
|
-
const result = await checkXpGateUpdate()
|
|
272
|
-
getLocalVersionOverride = null
|
|
273
|
-
|
|
274
|
-
// If cache matches local, action should be "noop" (cache hit).
|
|
275
|
-
// However, fetchNpmLatestVersion is a real network call that cannot
|
|
276
|
-
// be mocked here — if npm registry returns a different version,
|
|
277
|
-
// the cache-localVersion match still prevents re-check.
|
|
278
|
-
assert.equal(result.action, "noop")
|
|
279
|
-
assert.equal(result.localVersion, "0.9.2")
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
void it("ignores cache when local version changed (manual upgrade detected)", async () => {
|
|
283
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
284
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
285
|
-
ts: Date.now(),
|
|
286
|
-
localVersion: "0.9.1", // old cached version
|
|
287
|
-
remoteVersion: "0.9.1",
|
|
288
|
-
status: "current",
|
|
289
|
-
}))
|
|
290
|
-
|
|
291
|
-
// Simulate user manually upgraded npm package to 0.10.8
|
|
292
|
-
getLocalVersionOverride = () => "0.10.8"
|
|
293
|
-
|
|
294
|
-
const result = await checkXpGateUpdate()
|
|
295
|
-
getLocalVersionOverride = null
|
|
296
|
-
|
|
297
|
-
// Cache must be ignored because cached localVersion (0.9.1) ≠ actual (0.10.8)
|
|
298
|
-
// Falls through to network check. We got past the cache shortcut — that's the pass condition.
|
|
299
|
-
assert.ok(["noop", "upgraded", "error"].includes(result.action))
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
void it("handles cache expiry correctly", async () => {
|
|
303
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
304
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
305
|
-
ts: Date.now() - 86_400_000 - 3600_000, // 25h old
|
|
306
|
-
localVersion: "0.9.1",
|
|
307
|
-
remoteVersion: "0.9.2",
|
|
308
|
-
}))
|
|
309
|
-
|
|
310
|
-
const result = await checkXpGateUpdate()
|
|
311
|
-
// Stale cache should be ignored — should check npm registry
|
|
312
|
-
// If network check fails, returns noop with localVersion if found
|
|
313
|
-
assert.equal(result.action, "noop")
|
|
314
|
-
})
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// ── UPG-002: spawn-based upgrade tests ──
|
|
319
|
-
|
|
320
|
-
void describe("checkXpGateUpdate — spawn (UPG-002)", () => {
|
|
321
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-spawn-" + randomUUID())
|
|
322
|
-
const origHome = process.env.HOME
|
|
323
|
-
|
|
324
|
-
before(() => {
|
|
325
|
-
process.env.HOME = fakeHome
|
|
326
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
after(() => {
|
|
330
|
-
process.env.HOME = origHome
|
|
331
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
void it("returns upgraded with spawn-based npm install", async () => {
|
|
335
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
336
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
337
|
-
ts: Date.now() - 86_400_000 - 3600_000, // stale
|
|
338
|
-
localVersion: "0.9.1",
|
|
339
|
-
remoteVersion: "0.9.2",
|
|
340
|
-
}))
|
|
341
|
-
|
|
342
|
-
const result = await checkXpGateUpdate()
|
|
343
|
-
assert.ok(["noop", "upgraded", "error"].includes(result.action))
|
|
344
|
-
})
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
// ── UPG-003: readXpGateConfig isolated tests ──
|
|
348
|
-
|
|
349
|
-
void describe("readXpGateConfig", () => {
|
|
350
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-cfg-" + randomUUID())
|
|
351
|
-
const origHome = process.env.HOME
|
|
352
|
-
|
|
353
|
-
before(() => {
|
|
354
|
-
process.env.HOME = fakeHome
|
|
355
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
after(() => {
|
|
359
|
-
process.env.HOME = origHome
|
|
360
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
void it("returns null when no config.json exists", () => {
|
|
364
|
-
assert.equal(readXpGateConfig(), null)
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
void it("returns parsed config when config.json exists with autoUpgrade false", () => {
|
|
368
|
-
const cfgPath = join(fakeHome, ".xp-gate", "config.json")
|
|
369
|
-
writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: false }))
|
|
370
|
-
const cfg = readXpGateConfig()
|
|
371
|
-
assert.notEqual(cfg, null)
|
|
372
|
-
assert.equal(cfg!.autoUpgrade, false)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
void it("returns parsed config when config.json exists with autoUpgrade true", () => {
|
|
376
|
-
const cfgPath = join(fakeHome, ".xp-gate", "config.json")
|
|
377
|
-
writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: true }))
|
|
378
|
-
const cfg = readXpGateConfig()
|
|
379
|
-
assert.notEqual(cfg, null)
|
|
380
|
-
assert.equal(cfg!.autoUpgrade, true)
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
void it("returns null when config.json is malformed", () => {
|
|
384
|
-
const cfgPath = join(fakeHome, ".xp-gate", "config.json")
|
|
385
|
-
writeFileSync(cfgPath, "not-json")
|
|
386
|
-
assert.equal(readXpGateConfig(), null)
|
|
387
|
-
})
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
// ── UPG-003: opt-out config integration tests ──
|
|
391
|
-
|
|
392
|
-
void describe("checkXpGateUpdate — opt-out config integration (UPG-003)", () => {
|
|
393
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-cfg-int-" + randomUUID())
|
|
394
|
-
const origHome = process.env.HOME
|
|
395
|
-
|
|
396
|
-
before(() => {
|
|
397
|
-
process.env.HOME = fakeHome
|
|
398
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
after(() => {
|
|
402
|
-
process.env.HOME = origHome
|
|
403
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
void it("handles config.json with autoUpgrade false (no crash, graceful noop)", async () => {
|
|
407
|
-
const cfgPath = join(fakeHome, ".xp-gate", "config.json")
|
|
408
|
-
writeFileSync(cfgPath, JSON.stringify({ autoUpgrade: false }))
|
|
409
|
-
|
|
410
|
-
const result = await checkXpGateUpdate()
|
|
411
|
-
// Should not crash — returns noop because local install not found in fake home
|
|
412
|
-
assert.ok(["noop", "upgraded", "error"].includes(result.action))
|
|
413
|
-
})
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
// ── UPG-004: Fire-and-forget spawn → cache write test ──
|
|
417
|
-
|
|
418
|
-
void describe("checkXpGateUpdate — spawn completes and writes cache (UPG-004)", () => {
|
|
419
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-spawn-cache-" + randomUUID())
|
|
420
|
-
const origHome = process.env.HOME
|
|
421
|
-
|
|
422
|
-
before(() => {
|
|
423
|
-
process.env.HOME = fakeHome
|
|
424
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
after(() => {
|
|
428
|
-
process.env.HOME = origHome
|
|
429
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
void it("should write cache with status:current after spawn-based upgrade completes", async () => {
|
|
433
|
-
// WARNING: This test ACTUALLY spawns npm install -g.
|
|
434
|
-
// It's skipped in CI environments.
|
|
435
|
-
if (process.env.CI) {
|
|
436
|
-
console.log("SKIP: UPG-004 spawn test disabled in CI")
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Simulate: local is old, remote is newer → should trigger spawn
|
|
441
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
442
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
443
|
-
ts: Date.now() - 86_400_000 - 3600_000, // stale cache
|
|
444
|
-
localVersion: "0.9.0",
|
|
445
|
-
remoteVersion: "0.9.1",
|
|
446
|
-
}))
|
|
447
|
-
|
|
448
|
-
// Override local version to force upgrade path
|
|
449
|
-
getLocalVersionOverride = () => "0.9.0"
|
|
450
|
-
|
|
451
|
-
const result = await checkXpGateUpdate()
|
|
452
|
-
getLocalVersionOverride = null
|
|
453
|
-
|
|
454
|
-
// After fix: spawn is awaited, so cache is already written when function returns
|
|
455
|
-
assert.equal(result.action, "upgraded")
|
|
456
|
-
|
|
457
|
-
// Cache should have status:current immediately (no delay needed)
|
|
458
|
-
const finalCache = readXpGateCache()
|
|
459
|
-
if (finalCache) {
|
|
460
|
-
assert.equal(finalCache.status, "current",
|
|
461
|
-
"UPG-004 FAIL: spawn did not write status:current. The upgrade promise " +
|
|
462
|
-
"must be awaited so the cache reflects the completed install.")
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
// ── UPG-005: runBackgroundUpdates awaits checkXpGateUpdate ──
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Simulates runBackgroundUpdates to verify it properly awaits the upgrade.
|
|
471
|
-
* This test validates that the chat.message hook doesn't lose the upgrade.
|
|
472
|
-
*/
|
|
473
|
-
void describe("runBackgroundUpdates — await verification (UPG-005)", () => {
|
|
474
|
-
const fakeHome = join(tmpdir(), "xp-gate-upd-await-" + randomUUID())
|
|
475
|
-
const origHome = process.env.HOME
|
|
476
|
-
|
|
477
|
-
before(() => {
|
|
478
|
-
process.env.HOME = fakeHome
|
|
479
|
-
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
after(() => {
|
|
483
|
-
process.env.HOME = origHome
|
|
484
|
-
rmSync(fakeHome, { recursive: true, force: true })
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
void it("checkXpGateUpdate resolves its promise BEFORE returning result", async () => {
|
|
488
|
-
// If spawn is fire-and-forget, the function returns before spawn completes.
|
|
489
|
-
// This test measures: does the returned promise resolve before or after spawn?
|
|
490
|
-
if (process.env.CI) {
|
|
491
|
-
console.log("SKIP: UPG-005 spawn timing test disabled in CI")
|
|
492
|
-
return
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
496
|
-
writeFileSync(cachePath, JSON.stringify({
|
|
497
|
-
ts: Date.now() - 86_400_000 - 3600_000,
|
|
498
|
-
localVersion: "0.9.0",
|
|
499
|
-
remoteVersion: "0.9.1",
|
|
500
|
-
}))
|
|
501
|
-
|
|
502
|
-
getLocalVersionOverride = () => "0.9.0"
|
|
503
|
-
|
|
504
|
-
const startTime = Date.now()
|
|
505
|
-
const result = await checkXpGateUpdate()
|
|
506
|
-
const elapsed = Date.now() - startTime
|
|
507
|
-
getLocalVersionOverride = null
|
|
508
|
-
|
|
509
|
-
assert.equal(result.action, "upgraded")
|
|
510
|
-
|
|
511
|
-
// After fix: spawn is properly awaited, so elapsed >= 1000ms
|
|
512
|
-
console.log(`UPG-005: checkXpGateUpdate() returned in ${elapsed}ms`)
|
|
513
|
-
assert.ok(elapsed > 1000,
|
|
514
|
-
`UPG-005 FAIL: resolve took only ${elapsed}ms — spawn is NOT awaited. ` +
|
|
515
|
-
"The chat.message hook must await the upgrade promise so " +
|
|
516
|
-
"npm install completes and cache gets status:current.")
|
|
517
|
-
})
|
|
518
|
-
})
|