@boyingliu01/xp-gate 0.12.2 → 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.
Files changed (54) hide show
  1. package/adapters/cpp.sh +0 -0
  2. package/adapters/dart.sh +0 -0
  3. package/adapters/flutter.sh +0 -0
  4. package/adapters/go.sh +0 -0
  5. package/adapters/java.sh +0 -0
  6. package/adapters/kotlin.sh +0 -0
  7. package/adapters/objectivec.sh +0 -0
  8. package/adapters/powershell.sh +0 -0
  9. package/adapters/python.sh +0 -0
  10. package/adapters/shell.sh +0 -0
  11. package/adapters/swift.sh +0 -0
  12. package/adapters/typescript.sh +0 -0
  13. package/bin/xp-gate.js +19 -5
  14. package/gate-3.sh +0 -0
  15. package/gate-4.sh +0 -0
  16. package/gate-7.sh +0 -0
  17. package/gate-8.sh +0 -0
  18. package/gate-9.sh +0 -0
  19. package/lib/__tests__/update-hooks.test.js +573 -0
  20. package/lib/init.js +9 -6
  21. package/lib/sprint-status.js +35 -1
  22. package/lib/update-hooks.js +288 -0
  23. package/mock-policy/AGENTS.md +3 -3
  24. package/mutation/AGENTS.md +3 -3
  25. package/package.json +1 -1
  26. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  27. package/plugins/claude-code/skills/delphi-review/AGENTS.md +6 -6
  28. package/plugins/claude-code/skills/delphi-review/SKILL.md +34 -12
  29. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/opencode/package.json +1 -1
  32. package/plugins/opencode/skills/delphi-review/AGENTS.md +6 -6
  33. package/plugins/opencode/skills/delphi-review/SKILL.md +34 -12
  34. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  35. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  36. package/plugins/qoder/plugin.json +1 -1
  37. package/plugins/qoder/skills/delphi-review/AGENTS.md +6 -6
  38. package/plugins/qoder/skills/delphi-review/SKILL.md +198 -264
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  41. package/principles/AGENTS.md +3 -3
  42. package/skills/delphi-review/AGENTS.md +6 -6
  43. package/skills/delphi-review/SKILL.md +34 -12
  44. package/skills/sprint-flow/AGENTS.md +3 -3
  45. package/skills/test-specification-alignment/AGENTS.md +3 -3
  46. package/lib/__tests__/next-sprint.test.js +0 -231
  47. package/lib/next-sprint.js +0 -129
  48. package/lib/shared-phase-constants.d.ts +0 -17
  49. package/lib/shared-phase-constants.js +0 -77
  50. package/plugins/opencode/__tests__/tui-plugin.test.ts +0 -543
  51. package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +0 -562
  52. package/plugins/opencode/__tests__/xp-gate-update.test.ts +0 -518
  53. package/plugins/opencode/tui-plugin.ts +0 -414
  54. package/plugins/opencode/tui-plugin.tsx +0 -306
@@ -1,562 +0,0 @@
1
- /**
2
- * XP-Gate E2E Upgrade Test
3
- *
4
- * End-to-end tests for the OpenCode plugin's auto-upgrade flow:
5
- * checkXpGateUpdate → writeUpgradeNotice → chat.message hook
6
- *
7
- * These tests validate the FULL code path that was broken before the
8
- * fire-and-forget fix (commit 9bad3c1):
9
- * - spawn must be awaited (not discarded when hook returns)
10
- * - cache must be written with status:current after spawn completes
11
- * - upgrade-notice.json must be written for TUI display
12
- * - runBackgroundUpdates must return the upgrade message
13
- *
14
- * All tests use REAL `npm install -g` (skipped in CI).
15
- *
16
- * Function implementations are copied from plugins/opencode/index.ts
17
- * because importing from index.ts requires @opencode-ai/plugin runtime.
18
- * This mirrors the existing pattern in xp-gate-update.test.ts.
19
- *
20
- * @test E2E-001 Full runBackgroundUpdates flow
21
- * @test E2E-002 Await timing verification
22
- * @test E2E-003 Upgrade notice file content
23
- * @test E2E-004 Chat.message hook simulation
24
- * @test E2E-005 Cache integrity after spawn
25
- */
26
-
27
- import { describe, it, before, after } from "node:test"
28
- import assert from "node:assert/strict"
29
- import { randomUUID } from "node:crypto"
30
- import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"
31
- import { join } from "node:path"
32
- import { tmpdir, homedir } from "node:os"
33
- import { execSync, spawn } from "node:child_process"
34
-
35
- // ── Constants (must match index.ts) ──
36
-
37
- const CACHE_TTL_MS = 86_400_000 // 24h
38
- const FETCH_TIMEOUT_MS = 5_000
39
-
40
- const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
41
- const XP_GATE_CACHE_FILE = join(homedir(), ".xp-gate", "xp-gate-version-check.json")
42
- const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
43
- const UPGRADE_NOTICE_FILE = join(homedir(), ".xp-gate", "upgrade-notice.json")
44
-
45
- // ── Types (must match index.ts) ──
46
-
47
- type UpgradeResult = {
48
- action: "noop" | "upgraded" | "error"
49
- localVersion: string | null
50
- remoteVersion: string | null
51
- error?: string
52
- }
53
-
54
- type UpgradeNotice = {
55
- kind: "upgraded" | "outdated" | "error"
56
- localVersion: string | null
57
- remoteVersion: string | null
58
- message: string
59
- ts: number
60
- }
61
-
62
- type CacheEntry = {
63
- ts: number
64
- remoteVersion: string
65
- localVersion?: string
66
- status?: string
67
- }
68
-
69
- // ── Utilities (must match index.ts) ──
70
-
71
- function semverLt(a: string, b: string): boolean {
72
- const pa = a.replace(/^v/, "").split(".").map(Number)
73
- const pb = b.replace(/^v/, "").split(".").map(Number)
74
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
75
- const na = pa[i] ?? 0
76
- const nb = pb[i] ?? 0
77
- if (na !== nb) return na < nb
78
- }
79
- return false
80
- }
81
-
82
- async function fetchNpmLatestVersion(url: string): Promise<string | null> {
83
- const controller = new AbortController()
84
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
85
- try {
86
- const response = await fetch(url, { signal: controller.signal })
87
- if (!response.ok) return null
88
- const data: Record<string, unknown> = await response.json()
89
- return typeof data.latest === "string" ? data.latest : null
90
- } catch {
91
- return null
92
- } finally {
93
- clearTimeout(timer)
94
- }
95
- }
96
-
97
- function readCache(file: string): CacheEntry | null {
98
- try {
99
- if (!existsSync(file)) return null
100
- const raw = readFileSync(file, "utf8")
101
- const data = JSON.parse(raw)
102
- if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) return data
103
- return null
104
- } catch {
105
- return null
106
- }
107
- }
108
-
109
- function writeCache(file: string, data: object): void {
110
- try {
111
- mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
112
- writeFileSync(file + ".tmp", JSON.stringify(data), "utf8")
113
- try { rmSync(file) } catch {}
114
- writeFileSync(file, readFileSync(file + ".tmp", "utf8"), "utf8")
115
- try { rmSync(file + ".tmp") } catch {}
116
- } catch {
117
- // silent
118
- }
119
- }
120
-
121
- function readXpGateConfig(): { autoUpgrade?: boolean } | null {
122
- const cfgPath = join(homedir(), ".xp-gate", "config.json")
123
- try {
124
- if (!existsSync(cfgPath)) return null
125
- return JSON.parse(readFileSync(cfgPath, "utf8"))
126
- } catch {
127
- return null
128
- }
129
- }
130
-
131
- // ── Local version override (same pattern as xp-gate-update.test.ts) ──
132
-
133
- let getLocalVersionOverride: (() => string | null) | null = null
134
-
135
- function getLocalXpGateVersion(): string | null {
136
- if (getLocalVersionOverride) return getLocalVersionOverride()
137
- try {
138
- const globalRoot = execSync("npm root -g", { encoding: "utf8" }).trim()
139
- const pkg = JSON.parse(readFileSync(join(globalRoot, XP_GATE_NPM_PKG, "package.json"), "utf8"))
140
- return pkg.version || null
141
- } catch {
142
- return null
143
- }
144
- }
145
-
146
- // ── Core functions (MUST match index.ts EXACTLY) ──
147
-
148
- async function checkXpGateUpdate(): Promise<UpgradeResult> {
149
- const cached = readCache(XP_GATE_CACHE_FILE)
150
- const localVersion = getLocalXpGateVersion()
151
-
152
- if (cached?.status === "current" && cached.remoteVersion && localVersion && cached.localVersion === localVersion) {
153
- return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
154
- }
155
-
156
- if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
157
-
158
- const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
159
- if (!remoteVersion) return { action: "noop", localVersion, remoteVersion: null }
160
-
161
- if (!semverLt(localVersion, remoteVersion)) {
162
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
163
- return { action: "noop", localVersion, remoteVersion }
164
- }
165
-
166
- const config = readXpGateConfig()
167
- if (config?.autoUpgrade === false) {
168
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
169
- return { action: "noop", localVersion, remoteVersion }
170
- }
171
-
172
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
173
- try {
174
- const exitCode = await new Promise<number | null>((resolve, reject) => {
175
- const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
176
- stdio: "pipe",
177
- timeout: 120_000,
178
- })
179
- child.on("close", (code) => resolve(code))
180
- child.on("error", (err) => reject(err))
181
- })
182
- if (exitCode === 0) {
183
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
184
- return { action: "upgraded", localVersion, remoteVersion }
185
- }
186
- return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
187
- } catch (err) {
188
- const msg = err instanceof Error ? err.message : String(err)
189
- return { action: "error", localVersion, remoteVersion, error: msg }
190
- }
191
- }
192
-
193
- function writeUpgradeNotice(notice: UpgradeNotice): void {
194
- writeCache(UPGRADE_NOTICE_FILE, notice)
195
- }
196
-
197
- /**
198
- * Full background check — the function called by chat.message hook.
199
- * MUST match index.ts exactly.
200
- */
201
- async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
202
- void pluginDir // unused in this function, kept for signature match
203
- const result = await checkXpGateUpdate()
204
- // checkPluginUpdate is skipped here — it's not part of the xp-gate upgrade flow
205
-
206
- if (result.action === "upgraded") {
207
- const msg = `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
208
- writeUpgradeNotice({ kind: "upgraded", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
209
- return msg
210
- }
211
- if (result.action === "error") {
212
- const msg = `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
213
- writeUpgradeNotice({ kind: "error", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
214
- return msg
215
- }
216
- if (result.remoteVersion && result.localVersion && semverLt(result.localVersion, result.remoteVersion)) {
217
- const msg = `[XP-Gate] New version v${result.remoteVersion} available (you have v${result.localVersion})`
218
- writeUpgradeNotice({ kind: "outdated", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
219
- return msg
220
- }
221
- return null
222
- }
223
-
224
- // ── Helpers ──
225
-
226
- function readUpgradeNotice(): UpgradeNotice | null {
227
- try {
228
- if (!existsSync(UPGRADE_NOTICE_FILE)) return null
229
- return JSON.parse(readFileSync(UPGRADE_NOTICE_FILE, "utf8"))
230
- } catch {
231
- return null
232
- }
233
- }
234
-
235
- function assertInCI(): boolean {
236
- if (process.env.CI) {
237
- console.log(" [SKIP] E2E spawn test disabled in CI (real npm install required)")
238
- return true
239
- }
240
- return false
241
- }
242
-
243
- // ═══════════════════════════════════════════════════
244
- // E2E TESTS
245
- // ═══════════════════════════════════════════════════
246
-
247
- // ── Suite 1: Full runBackgroundUpdates E2E ──
248
-
249
- void describe("E2E: runBackgroundUpdates → upgrade + notice + cache (E2E-001)", () => {
250
- const fakeHome = join(tmpdir(), "xp-gate-e2e-001-" + randomUUID())
251
- const origHome = process.env.HOME
252
-
253
- before(() => {
254
- process.env.HOME = fakeHome
255
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
256
- })
257
-
258
- after(() => {
259
- process.env.HOME = origHome
260
- rmSync(fakeHome, { recursive: true, force: true })
261
- })
262
-
263
- void it("runBackgroundUpdates returns upgrade message when local < remote", async () => {
264
- if (assertInCI()) return
265
-
266
- // Seed a stale cache that will force a network check
267
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
268
- writeFileSync(cachePath, JSON.stringify({
269
- ts: Date.now() - 86_400_000 - 3600_000, // 25h stale
270
- localVersion: "0.8.0",
271
- remoteVersion: "0.8.1",
272
- }))
273
-
274
- // Override local version to be older than latest on npm
275
- getLocalVersionOverride = () => "0.8.0"
276
-
277
- const msg = await runBackgroundUpdates("/fake/plugin/dir")
278
- getLocalVersionOverride = null
279
-
280
- assert.ok(msg !== null, "E2E-001 FAIL: runBackgroundUpdates returned null — expected upgrade message")
281
- if (msg) {
282
- assert.ok(msg.includes("Auto-upgraded"), `E2E-001 FAIL: message missing 'Auto-upgraded': ${msg}`)
283
- }
284
-
285
- // Verify upgrade notice was written
286
- const notice = readUpgradeNotice()
287
- assert.ok(notice !== null, "E2E-001 FAIL: upgrade-notice.json was not written")
288
- if (notice) {
289
- assert.equal(notice.kind, "upgraded", `E2E-001 FAIL: notice kind=${notice.kind}, expected 'upgraded'`)
290
- assert.equal(notice.localVersion, "0.8.0")
291
- assert.ok(notice.remoteVersion !== null)
292
- assert.ok(notice.message.includes("Auto-upgraded"))
293
- assert.ok(notice.ts > 0)
294
- }
295
- })
296
-
297
- void it("cache is written with status:current immediately after runBackgroundUpdates returns", async () => {
298
- if (assertInCI()) return
299
-
300
- // Re-seed with stale cache to force another upgrade
301
- rmSync(join(fakeHome, ".xp-gate", "xp-gate-version-check.json"), { force: true })
302
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
303
- writeFileSync(cachePath, JSON.stringify({
304
- ts: Date.now() - 86_400_000 - 3600_000,
305
- localVersion: "0.8.0",
306
- remoteVersion: "0.8.1",
307
- }))
308
-
309
- getLocalVersionOverride = () => "0.8.0"
310
-
311
- await runBackgroundUpdates("/fake/plugin/dir")
312
- getLocalVersionOverride = null
313
-
314
- // After function returns, cache MUST have status:current
315
- // This is the KEY assertion — fire-and-forget would fail here
316
- const finalCache = readCache(XP_GATE_CACHE_FILE)
317
- if (finalCache) {
318
- assert.equal(finalCache.status, "current",
319
- "E2E-001 FAIL: cache status ≠ 'current' after runBackgroundUpdates returned. " +
320
- "If this fails, the spawn is NOT being awaited — the same bug as before the fix.")
321
- }
322
- })
323
- })
324
-
325
- // ── Suite 2: Await timing verification ──
326
-
327
- void describe("E2E: Await timing — resolve AFTER spawn (E2E-002)", () => {
328
- const fakeHome = join(tmpdir(), "xp-gate-e2e-002-" + randomUUID())
329
- const origHome = process.env.HOME
330
-
331
- before(() => {
332
- process.env.HOME = fakeHome
333
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
334
- })
335
-
336
- after(() => {
337
- process.env.HOME = origHome
338
- rmSync(fakeHome, { recursive: true, force: true })
339
- })
340
-
341
- void it("runBackgroundUpdates resolve time > 1000ms (spawn is awaited)", async () => {
342
- if (assertInCI()) return
343
-
344
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
345
- writeFileSync(cachePath, JSON.stringify({
346
- ts: Date.now() - 86_400_000 - 3600_000,
347
- localVersion: "0.8.0",
348
- remoteVersion: "0.8.1",
349
- }))
350
-
351
- getLocalVersionOverride = () => "0.8.0"
352
-
353
- const startTime = Date.now()
354
- const msg = await runBackgroundUpdates("/fake/plugin/dir")
355
- const elapsed = Date.now() - startTime
356
- getLocalVersionOverride = null
357
-
358
- console.log(` E2E-002: runBackgroundUpdates() returned in ${elapsed}ms`)
359
-
360
- if (msg?.includes("Auto-upgraded")) {
361
- // Only assert timing if upgrade actually happened (network call succeeded)
362
- assert.ok(elapsed > 1000,
363
- `E2E-002 FAIL: runBackgroundUpdates resolved in ${elapsed}ms — ` +
364
- "spawn was NOT awaited. The chat.message hook would lose the upgrade. " +
365
- "This is the fire-and-forget bug.")
366
- } else if (elapsed < 200) {
367
- // If no upgrade happened (network failure, etc.), elapsed should still be
368
- // reasonable for a network call (5s timeout for npm registry fetch)
369
- assert.ok(elapsed > 200,
370
- `E2E-002 FAIL: resolved in ${elapsed}ms with no upgrade — network check too fast?`)
371
- }
372
- })
373
- })
374
-
375
- // ── Suite 3: Upgrade notice file verification ──
376
-
377
- void describe("E2E: writeUpgradeNotice file content (E2E-003)", () => {
378
- const fakeHome = join(tmpdir(), "xp-gate-e2e-003-" + randomUUID())
379
- const origHome = process.env.HOME
380
-
381
- before(() => {
382
- process.env.HOME = fakeHome
383
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
384
- })
385
-
386
- after(() => {
387
- process.env.HOME = origHome
388
- rmSync(fakeHome, { recursive: true, force: true })
389
- })
390
-
391
- void it("notice file has correct schema (kind, localVersion, remoteVersion, message, ts)", async () => {
392
- if (assertInCI()) return
393
-
394
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
395
- writeFileSync(cachePath, JSON.stringify({
396
- ts: Date.now() - 86_400_000 - 3600_000,
397
- localVersion: "0.8.0",
398
- remoteVersion: "0.8.1",
399
- }))
400
-
401
- getLocalVersionOverride = () => "0.8.0"
402
- await runBackgroundUpdates("/fake/plugin/dir")
403
- getLocalVersionOverride = null
404
-
405
- const notice = readUpgradeNotice()
406
- assert.ok(notice !== null, "E2E-003 FAIL: notice file not written")
407
-
408
- if (notice) {
409
- // Schema validation
410
- assert.ok(["upgraded", "outdated", "error"].includes(notice.kind),
411
- `E2E-003 FAIL: invalid kind: ${notice.kind}`)
412
- assert.ok(typeof notice.localVersion === "string" || notice.localVersion === null,
413
- "E2E-003 FAIL: localVersion must be string or null")
414
- assert.ok(typeof notice.remoteVersion === "string" || notice.remoteVersion === null,
415
- "E2E-003 FAIL: remoteVersion must be string or null")
416
- assert.ok(typeof notice.message === "string" && notice.message.length > 0,
417
- "E2E-003 FAIL: message must be non-empty string")
418
- assert.ok(typeof notice.ts === "number" && notice.ts > 0,
419
- "E2E-003 FAIL: ts must be positive number")
420
- }
421
- })
422
-
423
- void it("notice file can be read back and parsed (no corruption)", async () => {
424
- if (assertInCI()) return
425
-
426
- // Verify the file is valid JSON (not corrupted by tmpfile write)
427
- const raw = readFileSync(UPGRADE_NOTICE_FILE, "utf8")
428
- let parsed: unknown = null
429
- assert.doesNotThrow(() => {
430
- parsed = JSON.parse(raw)
431
- }, "E2E-003 FAIL: notice file contains invalid JSON")
432
- assert.ok(parsed !== null)
433
- })
434
- })
435
-
436
- // ── Suite 4: Chat.message hook simulation (E2E-004) ──
437
-
438
- void describe("E2E: chat.message hook simulation (E2E-004)", () => {
439
- const fakeHome = join(tmpdir(), "xp-gate-e2e-004-" + randomUUID())
440
- const origHome = process.env.HOME
441
-
442
- before(() => {
443
- process.env.HOME = fakeHome
444
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
445
- })
446
-
447
- after(() => {
448
- process.env.HOME = origHome
449
- rmSync(fakeHome, { recursive: true, force: true })
450
- })
451
-
452
- void it("simulated chat.message hook: upgrade completes before hook returns", async () => {
453
- if (assertInCI()) return
454
-
455
- // This simulates the EXACT code path from index.ts lines 321-327:
456
- // "chat.message": async (_input: { message: string }) => {
457
- // if (!checked) {
458
- // checked = true
459
- // const msg = await runBackgroundUpdates(directory).catch(() => null)
460
- // if (msg) process.stderr.write(`${msg}\n`)
461
- // }
462
- // }
463
-
464
- let checked = false
465
- const stderrOutput: string[] = []
466
-
467
- // Simulate the hook callback (first chat.message invocation)
468
- const simulateHook = async (pluginDir: string): Promise<void> => {
469
- if (!checked) {
470
- checked = true
471
- const msg = await runBackgroundUpdates(pluginDir).catch(() => null)
472
- if (msg) stderrOutput.push(msg)
473
- }
474
- }
475
-
476
- // Setup: stale cache forces upgrade
477
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
478
- writeFileSync(cachePath, JSON.stringify({
479
- ts: Date.now() - 86_400_000 - 3600_000,
480
- localVersion: "0.8.0",
481
- remoteVersion: "0.8.1",
482
- }))
483
-
484
- getLocalVersionOverride = () => "0.8.0"
485
-
486
- const startTime = Date.now()
487
- await simulateHook("/fake/plugin/dir")
488
- const elapsed = Date.now() - startTime
489
- getLocalVersionOverride = null
490
-
491
- console.log(` E2E-004: simulateHook returned in ${elapsed}ms`)
492
-
493
- // The hook should have upgrade output written to stderr
494
- assert.ok(stderrOutput.length > 0,
495
- "E2E-004 FAIL: no stderr output from upgrade — chat.message hook would be silent")
496
-
497
- // If upgrade happened, check the content
498
- if (stderrOutput.length > 0) {
499
- const output = stderrOutput.join("\n")
500
- assert.ok(output.includes("Auto-upgraded") || output.includes("New version"),
501
- `E2E-004 FAIL: unexpected stderr output: ${output}`)
502
- }
503
-
504
- // Second hook call should NOT re-run (checked flag)
505
- const preCount = stderrOutput.length
506
- await simulateHook("/fake/plugin/dir")
507
- assert.equal(stderrOutput.length, preCount,
508
- "E2E-004 FAIL: second hook call re-ran upgrade check (checked flag broken)")
509
-
510
- // After hook returns, cache must be written
511
- const finalCache = readCache(XP_GATE_CACHE_FILE)
512
- if (finalCache && stderrOutput.some(s => s.includes("Auto-upgraded"))) {
513
- assert.equal(finalCache.status, "current",
514
- "E2E-004 FAIL: after chat.message hook, cache has no status:current. " +
515
- "The fire-and-forget bug would cause this.")
516
- }
517
- })
518
- })
519
-
520
- // ── Suite 5: Cache integrity after upgrade (E2E-005) ──
521
-
522
- void describe("E2E: Cache integrity after real npm install (E2E-005)", () => {
523
- const fakeHome = join(tmpdir(), "xp-gate-e2e-005-" + randomUUID())
524
- const origHome = process.env.HOME
525
-
526
- before(() => {
527
- process.env.HOME = fakeHome
528
- mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
529
- })
530
-
531
- after(() => {
532
- process.env.HOME = origHome
533
- rmSync(fakeHome, { recursive: true, force: true })
534
- })
535
-
536
- void it("cache is NOT corrupted after spawn (atomic write via tmpfile)", async () => {
537
- if (assertInCI()) return
538
-
539
- const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
540
- writeFileSync(cachePath, JSON.stringify({
541
- ts: Date.now() - 86_400_000 - 3600_000,
542
- localVersion: "0.8.0",
543
- remoteVersion: "0.8.1",
544
- }))
545
-
546
- getLocalVersionOverride = () => "0.8.0"
547
- await runBackgroundUpdates("/fake/plugin/dir")
548
- getLocalVersionOverride = null
549
-
550
- // Cache file must be valid JSON
551
- const raw = readFileSync(XP_GATE_CACHE_FILE, "utf8")
552
- let cache: unknown = null
553
- assert.doesNotThrow(() => {
554
- cache = JSON.parse(raw)
555
- }, "E2E-005 FAIL: cache file is not valid JSON — tmpfile write may be corrupted")
556
-
557
- // Cache must have required fields
558
- const c = cache as Record<string, unknown>
559
- assert.ok(typeof c.ts === "number", "E2E-005 FAIL: cache missing ts")
560
- assert.ok(typeof c.remoteVersion === "string", "E2E-005 FAIL: cache missing remoteVersion")
561
- })
562
- })