chore(release): v0.4.12 (versionCode 39) — global recent sessions + integration test (#145)
This commit is contained in:
@@ -94,8 +94,8 @@ android {
|
||||
applicationId 'cc.agentlabs.opencode'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 38
|
||||
versionName "0.4.11"
|
||||
versionCode 39
|
||||
versionName "0.4.12"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
4
app.json
4
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "OpenCode",
|
||||
"slug": "opencode-mobile",
|
||||
"version": "0.4.11",
|
||||
"version": "0.4.12",
|
||||
"orientation": "portrait",
|
||||
"scheme": "opencode",
|
||||
"userInterfaceStyle": "automatic",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"android": {
|
||||
"package": "cc.agentlabs.opencode",
|
||||
"versionCode": 38,
|
||||
"versionCode": 39,
|
||||
"usesCleartextTraffic": true,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
|
||||
5
distribution/changelogs/39.txt
Normal file
5
distribution/changelogs/39.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
v0.4.12 — Global recent sessions
|
||||
|
||||
• Recent Sessions now shows every session across all your projects automatically.
|
||||
• No need to pick a folder or directory first — the list is populated globally on connect.
|
||||
• Falls back to per-directory listing on older servers.
|
||||
@@ -1,3 +1,3 @@
|
||||
v0.4.11 — Release reliability fix
|
||||
v0.4.12 — Global recent sessions
|
||||
|
||||
This maintenance release aligns Android version metadata across every build path so Google Play, direct APK, and F-Droid packages report the same version. No runtime behavior changed.
|
||||
Recent Sessions now shows every session across all your projects automatically — no need to pick a folder or directory first. The list is populated globally as soon as you connect, and falls back to per-directory listing on older servers.
|
||||
|
||||
5
fastlane/metadata/android/en-US/changelogs/39.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/39.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
v0.4.12 — global recent sessions
|
||||
|
||||
- Recent Sessions now shows every session across all your projects automatically.
|
||||
- No need to pick a folder or directory first — the list is populated globally on connect.
|
||||
- Falls back to per-directory listing on older servers.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/mobile",
|
||||
"version": "0.4.11",
|
||||
"version": "0.4.12",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
|
||||
102
src/lib/session-list.integration.test.ts
Normal file
102
src/lib/session-list.integration.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// Integration test for the "global recent sessions" feature.
|
||||
//
|
||||
// Unlike session-list.test.ts (pure logic, injected fakes), this stands up the
|
||||
// real mock opencode server over HTTP and drives loadSessionList through the
|
||||
// SAME transport shape sdk.ts wires in production (fetch /experimental/session
|
||||
// first, fall back to /session on 404). It proves the feature works end-to-end
|
||||
// across the HTTP boundary: the Recent Sessions list is populated globally —
|
||||
// every session across every directory — WITHOUT the user picking a folder.
|
||||
//
|
||||
// Run: node --test src/lib/session-list.integration.test.ts
|
||||
|
||||
import { test, before, after } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { createMockOpencodeServer } from "../../tests/fixtures/mock-opencode-server.ts"
|
||||
import { loadSessionList, legacySessionQuery, type SessionListTransport } from "./session-list.ts"
|
||||
|
||||
const PORT = 45071
|
||||
let mock: ReturnType<typeof createMockOpencodeServer>
|
||||
let base: string
|
||||
|
||||
before(async () => {
|
||||
// --seed-sessions pre-populates two sessions in two DIFFERENT directories:
|
||||
// seed-default -> /mock/project (updated now-120s)
|
||||
// seed-other -> /mock/project/other-dir (updated now-60s, more recent)
|
||||
mock = createMockOpencodeServer({ port: PORT, seedSessions: true })
|
||||
await mock.listen()
|
||||
base = mock.url
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await mock.close()
|
||||
})
|
||||
|
||||
// The exact transport sdk.ts builds in production: prefer the global
|
||||
// experimental endpoint, signal fallback (null) only on 404.
|
||||
function realTransport(baseUrl: string): SessionListTransport {
|
||||
return {
|
||||
getExperimental: async () => {
|
||||
const r = await fetch(`${baseUrl}/experimental/session`, { headers: { Accept: "application/json" } })
|
||||
if (r.status === 404) return null
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json()
|
||||
},
|
||||
getLegacy: async (query) => {
|
||||
const r = await fetch(`${baseUrl}/session${query}`, { headers: { Accept: "application/json" } })
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test("global sessions: lists sessions from EVERY directory without picking a folder", async () => {
|
||||
const sessions = await loadSessionList(realTransport(base), { roots: true, limit: 50 })
|
||||
|
||||
// Both directories are represented — this is the whole feature.
|
||||
const ids = sessions.map((s) => s.id)
|
||||
assert.ok(ids.includes("seed-default"), "session from /mock/project must appear")
|
||||
assert.ok(ids.includes("seed-other"), "session from /mock/project/other-dir must appear")
|
||||
assert.equal(sessions.length, 2)
|
||||
|
||||
const dirs = new Set(sessions.map((s) => s.directory))
|
||||
assert.equal(dirs.size, 2, "sessions span two distinct directories (global, not directory-scoped)")
|
||||
|
||||
// Most-recently-updated first: seed-other (now-60s) before seed-default (now-120s).
|
||||
assert.equal(sessions[0].id, "seed-other")
|
||||
assert.equal(sessions[1].id, "seed-default")
|
||||
})
|
||||
|
||||
test("hit the experimental endpoint, not the directory-scoped one", async () => {
|
||||
// Proves WHY the feature was needed: a plain directory-less GET /session
|
||||
// (no ?roots, no x-opencode-directory header) is directory-scoped and returns
|
||||
// ONLY the default directory's session — the old behavior that left the list
|
||||
// empty/partial until a folder was chosen.
|
||||
const scoped = await (await fetch(`${base}/session`)).json()
|
||||
assert.equal(scoped.length, 1, "directory-scoped /session returns only the default dir")
|
||||
assert.equal(scoped[0].id, "seed-default")
|
||||
|
||||
// The global endpoint returns everything.
|
||||
const global = await (await fetch(`${base}/experimental/session`)).json()
|
||||
assert.equal(global.length, 2, "/experimental/session returns all sessions globally")
|
||||
})
|
||||
|
||||
test("older servers (404 on /experimental/session) fall back to /session and still list globally", async () => {
|
||||
// Simulate a server that predates /experimental/session: getExperimental
|
||||
// resolves null (as it would on a 404), forcing the legacy path.
|
||||
const legacyTransport: SessionListTransport = {
|
||||
getExperimental: async () => null,
|
||||
getLegacy: async (query) => {
|
||||
const r = await fetch(`${base}/session${query}`, { headers: { Accept: "application/json" } })
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json()
|
||||
},
|
||||
}
|
||||
|
||||
const sessions = await loadSessionList(legacyTransport, { roots: true, limit: 50 })
|
||||
// The mock's /session?roots=true returns all sessions, so the fallback still
|
||||
// produces a global list on old servers.
|
||||
assert.equal(sessions.length, 2)
|
||||
assert.deepEqual(new Set(sessions.map((s) => s.id)), new Set(["seed-default", "seed-other"]))
|
||||
// Sanity: the legacy query we sent is the exact one the old code sent.
|
||||
assert.equal(legacySessionQuery({ roots: true, limit: 50 }), "?roots=true&limit=50")
|
||||
})
|
||||
12
tests/fixtures/mock-opencode-server.ts
vendored
12
tests/fixtures/mock-opencode-server.ts
vendored
@@ -544,6 +544,16 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
|
||||
messagesBySession.set(id, [])
|
||||
return json(res, 200, session)
|
||||
}
|
||||
// Global "all sessions across every directory" list, mirroring the real
|
||||
// opencode server's GET /experimental/session. This is the endpoint the
|
||||
// client now PREFERS (src/lib/sdk.ts session.list -> loadSessionList): on a
|
||||
// real server a directory-less GET /session is directory-scoped and returns
|
||||
// [] when the active dir has no sessions, so the Recent Sessions list was
|
||||
// empty until the user picked a folder. /experimental/session always
|
||||
// returns every session, ignoring the x-opencode-directory header.
|
||||
if (method === "GET" && path === "/experimental/session") {
|
||||
return json(res, 200, Array.from(sessions.values()))
|
||||
}
|
||||
if (method === "GET" && path === "/session") {
|
||||
// Directory-less "all sessions across all projects" list: the app's
|
||||
// loadSessions() (src/stores/sessions.ts) uses clientForDirectory(undefined)
|
||||
@@ -551,6 +561,8 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
|
||||
// session.list). Only that combination returns sessions from every
|
||||
// directory; otherwise the list is scoped to the request's directory, so
|
||||
// directory-scoped and directory-less clients are actually distinguishable.
|
||||
// NOTE: this legacy path remains the 404-fallback for older servers that
|
||||
// predate /experimental/session.
|
||||
if (url.searchParams.get("roots") === "true") {
|
||||
return json(res, 200, Array.from(sessions.values()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user