Turns the manual QA ask ("verify DiffView + CodeBlock horizontal-scroll
on-device with a populated diff") into two automated layers:
1. Unit (deterministic, runs in `npm test` now): extracted the shared
ScrollView props into src/lib/scroll-config.ts (WIDE_CONTENT_SCROLL_CONFIG)
so DiffView.tsx and CodeBlock.tsx spread the SAME plain object their tests
assert on — no react-native-renderer needed. Added a source-scan
regression test (wide-content-scroll.regression.test.ts) that fails if
either component loses its ScrollView wiring or reintroduces
numberOfLines truncation.
2. E2E (Maestro): .maestro/flows/diff-scroll.yaml opens a session with a
pre-seeded wide edit-diff tool call and a wide fenced code block, then
swipes each horizontal ScrollView left and asserts the off-screen marker
text becomes visible. mock-opencode-server.ts gained a --seed-diff mode
that serves this session via GET /session/:id/message (pre-existing
history), not SSE — issue #90 (a separate SSE-render bug) is being fixed
independently, and this flow must not depend on it landing first. Wired
the new flow + port 4100 into run-e2e-flows.sh and activation-e2e.yml.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
6
.github/workflows/activation-e2e.yml
vendored
6
.github/workflows/activation-e2e.yml
vendored
@@ -120,12 +120,15 @@ jobs:
|
|||||||
# different directories, for all-sessions.yaml). 4099 is a fresh normal-mode
|
# different directories, for all-sessions.yaml). 4099 is a fresh normal-mode
|
||||||
# instance shared by directory-picker.yaml and variant-picker.yaml (its fake
|
# instance shared by directory-picker.yaml and variant-picker.yaml (its fake
|
||||||
# file tree / provider variants / project list don't affect each other).
|
# file tree / provider variants / project list don't affect each other).
|
||||||
|
# 4100 is normal mode + --seed-diff (one session with a pre-existing wide
|
||||||
|
# edit-diff tool call + wide code block, for diff-scroll.yaml / issue #21).
|
||||||
# All bind 0.0.0.0 so the emulator can reach them via 10.0.2.2.
|
# All bind 0.0.0.0 so the emulator can reach them via 10.0.2.2.
|
||||||
nohup node tests/fixtures/mock-opencode-server.ts --port 4096 > /tmp/mock-4096.log 2>&1 &
|
nohup node tests/fixtures/mock-opencode-server.ts --port 4096 > /tmp/mock-4096.log 2>&1 &
|
||||||
nohup node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth > /tmp/mock-4097.log 2>&1 &
|
nohup node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth > /tmp/mock-4097.log 2>&1 &
|
||||||
nohup node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions > /tmp/mock-4098.log 2>&1 &
|
nohup node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions > /tmp/mock-4098.log 2>&1 &
|
||||||
nohup node tests/fixtures/mock-opencode-server.ts --port 4099 > /tmp/mock-4099.log 2>&1 &
|
nohup node tests/fixtures/mock-opencode-server.ts --port 4099 > /tmp/mock-4099.log 2>&1 &
|
||||||
for port in 4096 4097 4098 4099; do
|
nohup node tests/fixtures/mock-opencode-server.ts --port 4100 --seed-diff > /tmp/mock-4100.log 2>&1 &
|
||||||
|
for port in 4096 4097 4098 4099 4100; do
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
if curl -sf --connect-timeout 1 -m 3 "http://127.0.0.1:${port}/global/health" > /dev/null 2>&1; then
|
if curl -sf --connect-timeout 1 -m 3 "http://127.0.0.1:${port}/global/health" > /dev/null 2>&1; then
|
||||||
echo "mock server on ${port} responded (may be 401, that's expected on 4097)"
|
echo "mock server on ${port} responded (may be 401, that's expected on 4097)"
|
||||||
@@ -167,6 +170,7 @@ jobs:
|
|||||||
echo "--- 4097 (fail-auth) ---"; cat /tmp/mock-4097.log || true
|
echo "--- 4097 (fail-auth) ---"; cat /tmp/mock-4097.log || true
|
||||||
echo "--- 4098 (seed-sessions) ---"; cat /tmp/mock-4098.log || true
|
echo "--- 4098 (seed-sessions) ---"; cat /tmp/mock-4098.log || true
|
||||||
echo "--- 4099 (normal, directory-picker + variant-picker) ---"; cat /tmp/mock-4099.log || true
|
echo "--- 4099 (normal, directory-picker + variant-picker) ---"; cat /tmp/mock-4099.log || true
|
||||||
|
echo "--- 4100 (seed-diff) ---"; cat /tmp/mock-4100.log || true
|
||||||
|
|
||||||
- name: Upload screenshots
|
- name: Upload screenshots
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
119
.maestro/flows/diff-scroll.yaml
Normal file
119
.maestro/flows/diff-scroll.yaml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
appId: cc.agentlabs.opencode
|
||||||
|
name: "DiffView + CodeBlock horizontal scroll - wide diff/code line reachable by swipe (closes #21)"
|
||||||
|
---
|
||||||
|
# GitHub issue #21 ("QA: verify DiffView + CodeBlock horizontal-scroll
|
||||||
|
# on-device with a populated diff"). Two rendering fixes shipped earlier were
|
||||||
|
# code-audited but never verified with real, wide content on a real
|
||||||
|
# (emulated) device:
|
||||||
|
# - src/components/chat/DiffView.tsx: diff lines render inside a horizontal
|
||||||
|
# ScrollView (previously truncated with numberOfLines={1}).
|
||||||
|
# - src/components/markdown/CodeBlock.tsx: code renders inside a horizontal
|
||||||
|
# ScrollView (previously long lines wrapped/mangled).
|
||||||
|
#
|
||||||
|
# The CI job starts `node tests/fixtures/mock-opencode-server.ts --port 4100
|
||||||
|
# --seed-diff` on the runner host BEFORE this flow runs. --seed-diff
|
||||||
|
# pre-populates ONE session (id "seed-diff", directory /mock/project — the
|
||||||
|
# default connection's directory) whose single assistant message already
|
||||||
|
# contains:
|
||||||
|
# - a completed `edit` tool part with a wide "add" diff line (renders via
|
||||||
|
# ToolCallCard -> EditDetail -> DiffView once the card is expanded)
|
||||||
|
# - a text part with a wide fenced code block (renders via Markdown ->
|
||||||
|
# CodeBlock)
|
||||||
|
# Both wide lines are ~220 filler characters (no spaces, so they can't
|
||||||
|
# word-wrap) followed by a distinct marker token. This is loaded via GET
|
||||||
|
# /session/seed-diff/message when the session is opened (selectSession),
|
||||||
|
# NOT delivered over SSE — issue #90 (a separate SSE-render bug) is being
|
||||||
|
# fixed independently, and this flow must not depend on it landing first.
|
||||||
|
#
|
||||||
|
# Caveat for whoever debugs a flake here: assertVisible/assertNotVisible on
|
||||||
|
# a substring deep inside ONE very wide, unwrapped Text node is coarser than
|
||||||
|
# it looks — Maestro (like most accessibility-tree-based mobile test
|
||||||
|
# frameworks) determines visibility per ELEMENT, not per glyph, and there are
|
||||||
|
# open Maestro issues about partially-visible elements being reported as
|
||||||
|
# fully visible (mobile-dev-inc/maestro #1275, #2411). The filler is made
|
||||||
|
# very wide (~220 chars, comfortably wider than any emulator screen at this
|
||||||
|
# monospace font size) specifically to keep the marker's actual rendered
|
||||||
|
# position off past the right edge at rest. Screenshots are taken at every
|
||||||
|
# step so a flake here can be diagnosed visually, not just from the
|
||||||
|
# assertion's pass/fail.
|
||||||
|
|
||||||
|
- launchApp:
|
||||||
|
clearState: true
|
||||||
|
- assertVisible:
|
||||||
|
id: "telemetry-consent-card"
|
||||||
|
- tapOn:
|
||||||
|
id: "telemetry-decline-button"
|
||||||
|
|
||||||
|
- assertVisible:
|
||||||
|
text: "No Connection"
|
||||||
|
- tapOn:
|
||||||
|
id: "add-connection-button"
|
||||||
|
- tapOn:
|
||||||
|
id: "connect-ip-input"
|
||||||
|
- inputText: "127.0.0.1:4100"
|
||||||
|
- hideKeyboard
|
||||||
|
- tapOn:
|
||||||
|
id: "connect-submit-button"
|
||||||
|
|
||||||
|
# 40s margin: see activation-positive.yaml — the connect handshake's own
|
||||||
|
# fetches are individually capped at 30s (src/lib/sdk.ts REQUEST_TIMEOUT_MS).
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "connection-status-dot"
|
||||||
|
timeout: 40000
|
||||||
|
- takeScreenshot: diffscroll-S1_connected
|
||||||
|
|
||||||
|
# The seeded session must appear on the home tab's own-directory list (no
|
||||||
|
# SSE, no roots=true — this is the plain GET /session directory-scoped path).
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
text: "Wide Diff Session"
|
||||||
|
timeout: 15000
|
||||||
|
- tapOn:
|
||||||
|
id: "session-item-seed-diff"
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "chat-message-input"
|
||||||
|
timeout: 15000
|
||||||
|
|
||||||
|
# History loaded via GET /session/seed-diff/message (not SSE) must already
|
||||||
|
# render both the tool card and the markdown code block.
|
||||||
|
- assertVisible:
|
||||||
|
text: "Edit wide_diff_target.ts"
|
||||||
|
- assertVisible:
|
||||||
|
text: "Here is a wide code sample"
|
||||||
|
- takeScreenshot: diffscroll-S2_history_loaded
|
||||||
|
|
||||||
|
# --- CodeBlock: the wide fenced code block is already expanded (no tap
|
||||||
|
# needed — Markdown renders it immediately, unlike the collapsible tool card).
|
||||||
|
- assertVisible:
|
||||||
|
id: "code-block-scroll"
|
||||||
|
- assertNotVisible:
|
||||||
|
text: "ZZZ_CODE_SCROLL_TARGET_ZZZ"
|
||||||
|
- takeScreenshot: diffscroll-S3_codeblock_marker_offscreen
|
||||||
|
- swipe:
|
||||||
|
from:
|
||||||
|
id: "code-block-scroll"
|
||||||
|
direction: LEFT
|
||||||
|
- assertVisible:
|
||||||
|
text: "ZZZ_CODE_SCROLL_TARGET_ZZZ"
|
||||||
|
- takeScreenshot: diffscroll-S4_codeblock_marker_revealed
|
||||||
|
|
||||||
|
# --- DiffView: lives inside the collapsible tool card's detail section, so
|
||||||
|
# expand it first.
|
||||||
|
- tapOn:
|
||||||
|
text: "Edit wide_diff_target.ts"
|
||||||
|
- assertVisible:
|
||||||
|
text: "src/wide_diff_target.ts"
|
||||||
|
- assertVisible:
|
||||||
|
id: "diff-view-scroll"
|
||||||
|
- assertNotVisible:
|
||||||
|
text: "ZZZ_DIFF_SCROLL_TARGET_ZZZ"
|
||||||
|
- takeScreenshot: diffscroll-S5_diffview_marker_offscreen
|
||||||
|
- swipe:
|
||||||
|
from:
|
||||||
|
id: "diff-view-scroll"
|
||||||
|
direction: LEFT
|
||||||
|
- assertVisible:
|
||||||
|
text: "ZZZ_DIFF_SCROLL_TARGET_ZZZ"
|
||||||
|
- takeScreenshot: diffscroll-S6_diffview_marker_revealed
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Runs the Maestro activation E2E flows against the mock opencode servers the
|
# Runs the Maestro activation E2E flows against the mock opencode servers the
|
||||||
# workflow started on the host (ports 4096-4099). Invoked as a single line from
|
# workflow started on the host (ports 4096-4100). Invoked as a single line from
|
||||||
# activation-e2e.yml's emulator-runner `script:` so cd/trap/loop actually work.
|
# activation-e2e.yml's emulator-runner `script:` so cd/trap/loop actually work.
|
||||||
#
|
#
|
||||||
# Networking: `adb reverse` maps each mock port so the emulator's own
|
# Networking: `adb reverse` maps each mock port so the emulator's own
|
||||||
@@ -11,14 +11,14 @@ set -uo pipefail
|
|||||||
|
|
||||||
ROOT="$(pwd)" # capture BEFORE any cd, so diag paths are absolute
|
ROOT="$(pwd)" # capture BEFORE any cd, so diag paths are absolute
|
||||||
APK="android/app/build/outputs/apk/release/app-release.apk"
|
APK="android/app/build/outputs/apk/release/app-release.apk"
|
||||||
FLOWS=(activation-positive activation-negative-401 directory-picker all-sessions variant-picker)
|
FLOWS=(activation-positive activation-negative-401 directory-picker all-sessions variant-picker diff-scroll)
|
||||||
mkdir -p "$ROOT/artifacts/screenshots" "$ROOT/artifacts/diag"
|
mkdir -p "$ROOT/artifacts/screenshots" "$ROOT/artifacts/diag"
|
||||||
|
|
||||||
echo "== installing APK =="
|
echo "== installing APK =="
|
||||||
adb install "$APK"
|
adb install "$APK"
|
||||||
|
|
||||||
echo "== forwarding mock ports into the emulator (adb reverse) =="
|
echo "== forwarding mock ports into the emulator (adb reverse) =="
|
||||||
for p in 4096 4097 4098 4099; do adb reverse "tcp:$p" "tcp:$p"; done
|
for p in 4096 4097 4098 4099 4100; do adb reverse "tcp:$p" "tcp:$p"; done
|
||||||
adb reverse --list
|
adb reverse --list
|
||||||
|
|
||||||
# Decisive probe: can the EMULATOR actually reach the host mock via 127.0.0.1?
|
# Decisive probe: can the EMULATOR actually reach the host mock via 127.0.0.1?
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { View, Text, StyleSheet, Platform, ScrollView } from "react-native"
|
import { View, Text, StyleSheet, Platform, ScrollView } from "react-native"
|
||||||
|
import { WIDE_CONTENT_SCROLL_CONFIG } from "../../lib/scroll-config"
|
||||||
|
|
||||||
const mono = Platform.OS === "ios" ? "Menlo" : "monospace"
|
const mono = Platform.OS === "ios" ? "Menlo" : "monospace"
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export function DiffView({ before, after, isDark }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.container, isDark && s.containerDark]}>
|
<View style={[s.container, isDark && s.containerDark]}>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator>
|
<ScrollView {...WIDE_CONTENT_SCROLL_CONFIG} testID="diff-view-scroll">
|
||||||
<View>
|
<View>
|
||||||
{lines.map((line, idx) => (
|
{lines.map((line, idx) => (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Platform, ScrollView } from "react-native"
|
import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Platform, ScrollView } from "react-native"
|
||||||
import * as Clipboard from "expo-clipboard"
|
import * as Clipboard from "expo-clipboard"
|
||||||
|
import { WIDE_CONTENT_SCROLL_CONFIG } from "../../lib/scroll-config"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
code: string
|
code: string
|
||||||
@@ -27,7 +28,7 @@ export function CodeBlock({ code, language }: Props) {
|
|||||||
<Text style={[styles.copyBtn, isDark && styles.copyBtnDark]}>{copied ? "Copied!" : "Copy"}</Text>
|
<Text style={[styles.copyBtn, isDark && styles.copyBtnDark]}>{copied ? "Copied!" : "Copy"}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator contentContainerStyle={styles.codeScroll}>
|
<ScrollView {...WIDE_CONTENT_SCROLL_CONFIG} testID="code-block-scroll" contentContainerStyle={styles.codeScroll}>
|
||||||
<Text style={[styles.code, isDark && styles.codeDark]} selectable>
|
<Text style={[styles.code, isDark && styles.codeDark]} selectable>
|
||||||
{code}
|
{code}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
45
src/components/wide-content-scroll.regression.test.ts
Normal file
45
src/components/wide-content-scroll.regression.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { test } from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
// GitHub issue #21 ("QA: verify DiffView + CodeBlock horizontal-scroll
|
||||||
|
// on-device with a populated diff"). The underlying fixes were:
|
||||||
|
// - src/components/chat/DiffView.tsx: wrap diff lines in a horizontal
|
||||||
|
// ScrollView (previously truncated with numberOfLines={1}).
|
||||||
|
// - src/components/markdown/CodeBlock.tsx: wrap code in a horizontal
|
||||||
|
// ScrollView (previously long lines wrapped/mangled).
|
||||||
|
//
|
||||||
|
// This repo's runtime is React Native, so these components can't be rendered
|
||||||
|
// with node:test (no react-test-renderer / RN jest preset is set up here —
|
||||||
|
// see package.json's "test" script, which only globs plain .test.ts files).
|
||||||
|
// Instead of skipping component-level coverage, this test reads the actual
|
||||||
|
// .tsx source and asserts on its structure: it's a plain-text/regex check,
|
||||||
|
// but it directly targets the two markers that would prove a regression —
|
||||||
|
// (a) the ScrollView wiring, (b) reintroducing line-truncation — so it can't
|
||||||
|
// pass on a source file where the fix was reverted.
|
||||||
|
|
||||||
|
function readComponent(relativePath: string): string {
|
||||||
|
const dir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
return readFileSync(path.join(dir, relativePath), "utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("DiffView wraps diff lines in the shared horizontal-scroll ScrollView", () => {
|
||||||
|
const src = readComponent("chat/DiffView.tsx")
|
||||||
|
assert.match(src, /<ScrollView\s+\{\.\.\.WIDE_CONTENT_SCROLL_CONFIG\}/)
|
||||||
|
assert.doesNotMatch(src, /numberOfLines/, "DiffView must not truncate diff line text with numberOfLines")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CodeBlock wraps code in the shared horizontal-scroll ScrollView", () => {
|
||||||
|
const src = readComponent("markdown/CodeBlock.tsx")
|
||||||
|
assert.match(src, /<ScrollView\s+\{\.\.\.WIDE_CONTENT_SCROLL_CONFIG\}/)
|
||||||
|
assert.doesNotMatch(src, /numberOfLines/, "CodeBlock must not truncate code text with numberOfLines")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("DiffView and CodeBlock both source the config from the shared, unit-tested module", () => {
|
||||||
|
const diffView = readComponent("chat/DiffView.tsx")
|
||||||
|
const codeBlock = readComponent("markdown/CodeBlock.tsx")
|
||||||
|
assert.match(diffView, /from ["']\.\.\/\.\.\/lib\/scroll-config["']/)
|
||||||
|
assert.match(codeBlock, /from ["']\.\.\/\.\.\/lib\/scroll-config["']/)
|
||||||
|
})
|
||||||
26
src/lib/scroll-config.test.ts
Normal file
26
src/lib/scroll-config.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { test } from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { WIDE_CONTENT_SCROLL_CONFIG } from "./scroll-config.ts"
|
||||||
|
|
||||||
|
// GitHub issue #21: DiffView + CodeBlock must render wide content in a
|
||||||
|
// horizontally-scrollable container (not wrapped, not truncated). Both
|
||||||
|
// components spread WIDE_CONTENT_SCROLL_CONFIG onto their ScrollView (see
|
||||||
|
// src/components/chat/DiffView.tsx and src/components/markdown/CodeBlock.tsx)
|
||||||
|
// so asserting on this object here is asserting on the actual runtime props,
|
||||||
|
// not a parallel copy that can drift out of sync.
|
||||||
|
|
||||||
|
test("wide-content scroll config enables horizontal scrolling", () => {
|
||||||
|
assert.equal(WIDE_CONTENT_SCROLL_CONFIG.horizontal, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("wide-content scroll config shows the horizontal scroll indicator", () => {
|
||||||
|
// Regression guard: a container that scrolls but hides its indicator is
|
||||||
|
// easy to mistake for content that simply doesn't overflow. Keep the
|
||||||
|
// indicator visible so on-device QA (and screenshots) can tell scrollable
|
||||||
|
// content apart from clipped/truncated content.
|
||||||
|
assert.equal(WIDE_CONTENT_SCROLL_CONFIG.showsHorizontalScrollIndicator, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("wide-content scroll config has no unexpected keys", () => {
|
||||||
|
assert.deepEqual(Object.keys(WIDE_CONTENT_SCROLL_CONFIG).sort(), ["horizontal", "showsHorizontalScrollIndicator"])
|
||||||
|
})
|
||||||
18
src/lib/scroll-config.ts
Normal file
18
src/lib/scroll-config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Pure (no React Native imports) horizontal-scroll configuration shared by
|
||||||
|
// src/components/chat/DiffView.tsx and src/components/markdown/CodeBlock.tsx.
|
||||||
|
//
|
||||||
|
// GitHub issue #21: wide diff lines and wide code-block lines must render in
|
||||||
|
// a horizontally-scrollable container instead of being wrap-broken or
|
||||||
|
// truncated with `numberOfLines`. Centralizing the actual runtime props in
|
||||||
|
// one plain object lets that decision be unit-tested with node:test (no React
|
||||||
|
// Native renderer needed) while both components spread the SAME object onto
|
||||||
|
// their ScrollView, so the test and the real components can't drift apart.
|
||||||
|
export interface HorizontalScrollConfig {
|
||||||
|
horizontal: true
|
||||||
|
showsHorizontalScrollIndicator: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WIDE_CONTENT_SCROLL_CONFIG: HorizontalScrollConfig = {
|
||||||
|
horizontal: true,
|
||||||
|
showsHorizontalScrollIndicator: true,
|
||||||
|
}
|
||||||
118
tests/fixtures/mock-opencode-server.ts
vendored
118
tests/fixtures/mock-opencode-server.ts
vendored
@@ -68,6 +68,7 @@
|
|||||||
// node tests/fixtures/mock-opencode-server.ts --port 4096
|
// node tests/fixtures/mock-opencode-server.ts --port 4096
|
||||||
// node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth
|
// node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth
|
||||||
// node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions
|
// node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions
|
||||||
|
// node tests/fixtures/mock-opencode-server.ts --port 4100 --seed-diff
|
||||||
|
|
||||||
import http from "node:http"
|
import http from "node:http"
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
@@ -88,6 +89,20 @@ export interface MockServerOptions {
|
|||||||
* open regression for GitHub issues #46/#48).
|
* open regression for GitHub issues #46/#48).
|
||||||
*/
|
*/
|
||||||
seedSessions?: boolean
|
seedSessions?: boolean
|
||||||
|
/**
|
||||||
|
* Pre-populate a session (id "seed-diff", in DEFAULT_DIRECTORY so it shows
|
||||||
|
* up on the normal home-tab session list) with ONE assistant message that
|
||||||
|
* already contains a wide `edit` tool part (renders via DiffView) and a
|
||||||
|
* wide fenced code block in its text (renders via CodeBlock) — used by
|
||||||
|
* .maestro/flows/diff-scroll.yaml (GitHub issue #21) to prove both
|
||||||
|
* components' horizontal ScrollView actually scrolls on-device.
|
||||||
|
*
|
||||||
|
* Deliberately seeded as pre-existing history, fetched via GET
|
||||||
|
* /session/:id/message, NOT delivered over SSE — the positive-flow SSE
|
||||||
|
* render bug (issue #90) is being fixed separately, and this flow must not
|
||||||
|
* depend on it landing first.
|
||||||
|
*/
|
||||||
|
seedDiff?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredSession {
|
interface StoredSession {
|
||||||
@@ -106,6 +121,16 @@ interface StoredPart {
|
|||||||
messageID: string
|
messageID: string
|
||||||
type: string
|
type: string
|
||||||
text?: string
|
text?: string
|
||||||
|
// Tool part fields (type: "tool") — mirrors src/lib/sdk.ts's Part interface,
|
||||||
|
// needed to seed a populated `edit` tool call for --seed-diff.
|
||||||
|
tool?: string
|
||||||
|
callID?: string
|
||||||
|
state?: {
|
||||||
|
status: "pending" | "running" | "completed" | "error"
|
||||||
|
input?: unknown
|
||||||
|
output?: unknown
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredMessageInfo {
|
interface StoredMessageInfo {
|
||||||
@@ -166,13 +191,97 @@ export const FAKE_SERVER_PROJECTS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Exported so .maestro/flows/diff-scroll.yaml's assertions and this file's
|
||||||
|
// own seeding logic share one source of truth for what's "off-screen until
|
||||||
|
// you scroll" in the seeded diff/code content.
|
||||||
|
export const SEED_DIFF_SESSION_ID = "seed-diff"
|
||||||
|
export const SEED_DIFF_TITLE = "Wide Diff Session"
|
||||||
|
export const SEED_DIFF_TOOL_TITLE = "Edit wide_diff_target.ts"
|
||||||
|
export const SEED_DIFF_LINE_MARKER = "ZZZ_DIFF_SCROLL_TARGET_ZZZ"
|
||||||
|
export const SEED_CODE_LINE_MARKER = "ZZZ_CODE_SCROLL_TARGET_ZZZ"
|
||||||
|
|
||||||
|
// Long, space-free filler so the RN <Text> can't word-wrap it — it just
|
||||||
|
// overflows its parent, which is exactly what the horizontal ScrollView (the
|
||||||
|
// fix under test) exists to make scrollable instead of clipped/truncated.
|
||||||
|
const WIDE_FILLER = "x".repeat(220)
|
||||||
|
|
||||||
export function createMockOpencodeServer(opts: MockServerOptions) {
|
export function createMockOpencodeServer(opts: MockServerOptions) {
|
||||||
const { port, failAuth = false, replyText = DEFAULT_REPLY_TEXT, replyDelayMs = 300, seedSessions = false } = opts
|
const {
|
||||||
|
port,
|
||||||
|
failAuth = false,
|
||||||
|
replyText = DEFAULT_REPLY_TEXT,
|
||||||
|
replyDelayMs = 300,
|
||||||
|
seedSessions = false,
|
||||||
|
seedDiff = false,
|
||||||
|
} = opts
|
||||||
|
|
||||||
const sessions = new Map<string, StoredSession>()
|
const sessions = new Map<string, StoredSession>()
|
||||||
const messagesBySession = new Map<string, StoredMessage[]>()
|
const messagesBySession = new Map<string, StoredMessage[]>()
|
||||||
const sseClients = new Set<http.ServerResponse>()
|
const sseClients = new Set<http.ServerResponse>()
|
||||||
|
|
||||||
|
if (seedDiff) {
|
||||||
|
const now = Date.now()
|
||||||
|
const session: StoredSession = {
|
||||||
|
id: SEED_DIFF_SESSION_ID,
|
||||||
|
slug: "seed-diff",
|
||||||
|
projectID: "mock-project",
|
||||||
|
directory: DEFAULT_DIRECTORY,
|
||||||
|
title: SEED_DIFF_TITLE,
|
||||||
|
version: "0.0.0-mock",
|
||||||
|
time: { created: now - 30_000, updated: now - 20_000 },
|
||||||
|
}
|
||||||
|
sessions.set(session.id, session)
|
||||||
|
|
||||||
|
const messageID = "seed-diff-msg"
|
||||||
|
const info: StoredMessageInfo = {
|
||||||
|
id: messageID,
|
||||||
|
sessionID: session.id,
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: now - 25_000, completed: now - 20_000 },
|
||||||
|
modelID: "mock-model",
|
||||||
|
providerID: "mock",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populated `edit` tool call: renders via ToolCallCard -> EditDetail ->
|
||||||
|
// DiffView (src/components/chat/DiffView.tsx) once the card is expanded.
|
||||||
|
// One short unchanged line + one wide added line, so DiffView's diff has
|
||||||
|
// both a context row and an "add" row whose text overflows the ScrollView.
|
||||||
|
const toolPart: StoredPart = {
|
||||||
|
id: "seed-diff-tool",
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID,
|
||||||
|
type: "tool",
|
||||||
|
tool: "edit",
|
||||||
|
callID: "seed-diff-call-1",
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
title: SEED_DIFF_TOOL_TITLE,
|
||||||
|
input: {
|
||||||
|
filePath: "src/wide_diff_target.ts",
|
||||||
|
oldString: "const shortLine = 1",
|
||||||
|
newString: `const shortLine = 1\nconst wideLine = "${WIDE_FILLER}${SEED_DIFF_LINE_MARKER}"`,
|
||||||
|
},
|
||||||
|
output: "applied",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wide fenced code block in the reply text: renders via
|
||||||
|
// Markdown -> CustomRenderer.code -> CodeBlock
|
||||||
|
// (src/components/markdown/CodeBlock.tsx).
|
||||||
|
const textPart: StoredPart = {
|
||||||
|
id: "seed-diff-text",
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID,
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
"Here is a wide code sample:\n\n```typescript\n" +
|
||||||
|
`const wideCodeLine = "${WIDE_FILLER}${SEED_CODE_LINE_MARKER}"\n` +
|
||||||
|
"```\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesBySession.set(session.id, [{ info, parts: [toolPart, textPart] }])
|
||||||
|
}
|
||||||
|
|
||||||
if (seedSessions) {
|
if (seedSessions) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const seedDefault: StoredSession = {
|
const seedDefault: StoredSession = {
|
||||||
@@ -539,12 +648,13 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv: string[]): { port: number; failAuth: boolean; seedSessions: boolean } {
|
function parseArgs(argv: string[]): { port: number; failAuth: boolean; seedSessions: boolean; seedDiff: boolean } {
|
||||||
const opts = { port: 4096, failAuth: false, seedSessions: false }
|
const opts = { port: 4096, failAuth: false, seedSessions: false, seedDiff: false }
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
if (argv[i] === "--port") opts.port = Number(argv[++i])
|
if (argv[i] === "--port") opts.port = Number(argv[++i])
|
||||||
else if (argv[i] === "--fail-auth") opts.failAuth = true
|
else if (argv[i] === "--fail-auth") opts.failAuth = true
|
||||||
else if (argv[i] === "--seed-sessions") opts.seedSessions = true
|
else if (argv[i] === "--seed-sessions") opts.seedSessions = true
|
||||||
|
else if (argv[i] === "--seed-diff") opts.seedDiff = true
|
||||||
}
|
}
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
@@ -557,7 +667,7 @@ if (invokedDirectly) {
|
|||||||
const mock = createMockOpencodeServer(opts)
|
const mock = createMockOpencodeServer(opts)
|
||||||
mock.listen().then(() => {
|
mock.listen().then(() => {
|
||||||
console.log(
|
console.log(
|
||||||
`[mock-opencode-server] listening on ${mock.url} (failAuth=${opts.failAuth}, seedSessions=${opts.seedSessions})`,
|
`[mock-opencode-server] listening on ${mock.url} (failAuth=${opts.failAuth}, seedSessions=${opts.seedSessions}, seedDiff=${opts.seedDiff})`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const shutdown = () => {
|
const shutdown = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user