agents: a Codex left on the old list is named, with how to reopen it — the Codex app (⌘Q: closing its window keeps it running on macOS), an editor's Codex, the CLI's background app-server, a terminal's codex (miaopasi on Discord: "已接入 · 重开 Codex 后生效" stayed after restarting Codex)
Agent.Stale counted every process matching (^|/)codex( |$) that started before magpie's last change, and the row only said "reopen Codex". Several of those don't end when Codex is "reopened": ChatGPT.app's main process registers window-all-closed so a packaged app on darwin doesn't quit (read from /Applications/ChatGPT.app/Contents/Resources/app.asar), so closing the window leaves its app-server (CodexCLI.app/.../codex app-server) running; an editor extension's codex app-server, the CLI's --managed-daemon (ppid 1) and a codex in another terminal run on through a reopened app. One app also counted twice: its app-server and its `codex exec-server --remote` both match (this Mac's ps). Agent.StaleCopies gives each copy with its kind (app / ide / daemon / cli, from the command line) and start, one app's processes as one copy; Stale is its length. /api/state carries staleCopies, and the opened row under "N running Codex still have the old list" says, per copy, what it is, when it started and how to end it: ⌘Q for the app, the editor's window reload, `codex app-server daemon restart` for the daemon, quit for a terminal's. Strings in zh, zh-TW, ja, de. TestCodexStaleCopiesSayWhichIsLeft (an app with its exec-server, VS Code's Codex, this home's daemon, an old and a new terminal codex: app, ide, daemon, cli; Stale 4) fails on the old logic (5 copies, no kinds). agent-stale-copies.test.cjs (Chromium and WebKit, en/zh/zh-TW/ja/de, 980 and 440px) fails on the old app.js (no such lines). Run: go vet -tags nogui (darwin, GOOS=linux, GOOS=windows) ./internal/agent ./internal/gui; go test -tags nogui ./internal/agent ./internal/gui (temp HOME); node --test internal/gui/tests/agent-*.test.cjs gui-ja gui-de gui-zh-tw i18n-keys codex-model-order: 202 pass, both engines. Not tried with the reporter's Codex: which copy is left on their Mac isn't known.
This commit is contained in:
1 parent
40c5b92a93
commit
2f74a963d7
7 files changed
+249
-8
No files matched your search
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -64,3 +65,58 @@ func TestCodexStaleOnlyForMagpiesChange(t *testing.T) {
|
||||
t.Fatalf("asked again: %d stale, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
// miaopasi on Discord: the Codex row stayed "已接入 · 重开 Codex 后生效"
|
||||
// after Codex was restarted. On macOS closing the Codex app's window
|
||||
// leaves the app running, its app-server with it; an editor's Codex and
|
||||
// the CLI's managed daemon run on through a reopened app too. Each copy
|
||||
// left is told apart, so the row can say which one and how it is reopened,
|
||||
// and one app's processes are one copy, not one per process.
|
||||
func TestCodexStaleCopiesSayWhichIsLeft(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||
t.Setenv("CODEX_HOME", "")
|
||||
dir := filepath.Join(home, ".codex")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
cfg := filepath.Join(dir, "config.toml")
|
||||
os.WriteFile(cfg, []byte("model = \"gpt-6.1-sol\"\nopenai_base_url = \"http://127.0.0.1:3425/backend-api/codex\"\n"), 0o600)
|
||||
then := time.Now().Add(-2 * time.Hour)
|
||||
os.Chtimes(cfg, then, then)
|
||||
|
||||
was := running
|
||||
t.Cleanup(func() { running = was })
|
||||
app := "/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex"
|
||||
running = func(string) []runningProc {
|
||||
return []runningProc{
|
||||
// the Codex app, its window closed and opened again: the app
|
||||
// and its processes from three days ago
|
||||
{72 * time.Hour, app + " -c features.code_mode_host=true app-server --analytics-default-enabled"},
|
||||
{72*time.Hour - 10*time.Second, app + " exec-server --remote https://codex-cloud-environments.chatgpt.com/api"},
|
||||
// VS Code's Codex
|
||||
{5 * time.Hour, home + "/.vscode/extensions/openai.chatgpt-26.1003.0-darwin-arm64/bin/macos-aarch64/codex app-server --analytics-default-enabled"},
|
||||
// the CLI's daemon for this home
|
||||
{4 * time.Hour, dir + "/packages/app-server-daemon/releases/0.160.0-aarch64-apple-darwin/bin/codex app-server --listen unix:// --managed-daemon"},
|
||||
// a codex in a terminal from before, and one opened since
|
||||
{3 * time.Hour, "/opt/homebrew/bin/codex"},
|
||||
{time.Minute, "/opt/homebrew/bin/codex"},
|
||||
}
|
||||
}
|
||||
a := codex(home)
|
||||
got := a.StaleCopies()
|
||||
var kinds []string
|
||||
for _, c := range got {
|
||||
kinds = append(kinds, c.Kind)
|
||||
}
|
||||
if want := []string{"app", "ide", "daemon", "cli"}; strings.Join(kinds, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("copies: %v, want %v", kinds, want)
|
||||
}
|
||||
if d := time.Since(got[0].Since); d < 72*time.Hour-time.Minute || d > 72*time.Hour+time.Minute {
|
||||
t.Fatalf("the app's copy started %v ago, want the app-server's 72h", d)
|
||||
}
|
||||
if n := a.Stale(); n != 4 {
|
||||
t.Fatalf("Stale: %d, want 4 (one app, not one per process)", n)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -165,32 +166,92 @@ func (a *Agent) changedAt(files []string, reads func() string) time.Time {
|
||||
// Stale is how many copies of the agent are running that started before
|
||||
// magpie last changed what it reads at start: they still have the list
|
||||
// they started with until reopened. Zero where it can't be told (Windows).
|
||||
func (a *Agent) Stale() int { return len(a.StaleCopies()) }
|
||||
|
||||
// StaleCopy is a copy of an agent still running on the list it started
|
||||
// with: what kind of copy it is, which says how it is reopened, and when
|
||||
// it started.
|
||||
type StaleCopy struct {
|
||||
// Kind, for Codex: "app" (the ChatGPT or Codex desktop app, its
|
||||
// app-server and helpers), "ide" (an editor extension's), "daemon"
|
||||
// (the background app-server the CLI leaves running) or "cli"; ""
|
||||
// for other agents
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Since time.Time `json:"since"`
|
||||
}
|
||||
|
||||
// StaleCopies are the copies Stale counts. Reopening one kind doesn't end
|
||||
// another: on macOS the Codex app keeps running, its app-server with it,
|
||||
// when its window is closed (its window-all-closed doesn't quit a packaged
|
||||
// app on darwin), an editor's Codex runs until the editor's window is
|
||||
// reloaded, and the CLI's managed daemon runs on with ppid 1 until it is
|
||||
// restarted itself. So each is told apart for the row to say which one is
|
||||
// left. One app's processes (its app-server, its exec-server) are one copy.
|
||||
// One run from another home's agent dir (an app-server daemon Codex left
|
||||
// running for a CODEX_HOME of its own, under its packages) reads other
|
||||
// files, and isn't counted.
|
||||
func (a *Agent) Stale() int {
|
||||
func (a *Agent) StaleCopies() []StaleCopy {
|
||||
pats, files, reads := a.startsWith()
|
||||
if len(pats) == 0 || a.WSL != "" || runtime.GOOS == "windows" {
|
||||
return 0
|
||||
return nil
|
||||
}
|
||||
changed := a.changedAt(files, reads)
|
||||
if changed.IsZero() {
|
||||
return 0
|
||||
return nil
|
||||
}
|
||||
dir := filepath.Dir(a.Path)
|
||||
others := "/" + filepath.Base(dir) + "/"
|
||||
n := 0
|
||||
var out []StaleCopy
|
||||
apps := map[string]int{}
|
||||
for _, pat := range pats {
|
||||
for _, p := range running(pat) {
|
||||
if strings.Contains(p.cmd, others) && !strings.Contains(p.cmd, dir+"/") {
|
||||
continue
|
||||
}
|
||||
if time.Now().Add(-p.up).Before(changed.Add(-time.Second)) {
|
||||
n++
|
||||
since := time.Now().Add(-p.up)
|
||||
if !since.Before(changed.Add(-time.Second)) {
|
||||
continue
|
||||
}
|
||||
c := StaleCopy{Since: since}
|
||||
if a.ID == "codex" {
|
||||
var app string
|
||||
c.Kind, app = codexCopyKind(p.cmd)
|
||||
if app != "" {
|
||||
if i, ok := apps[app]; ok {
|
||||
if since.Before(out[i].Since) {
|
||||
out[i].Since = since
|
||||
}
|
||||
continue
|
||||
}
|
||||
apps[app] = len(out)
|
||||
}
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return n
|
||||
return out
|
||||
}
|
||||
|
||||
// codexCopyKind tells what runs a Codex process from its command line, and
|
||||
// for one of a desktop app the app's bundle.
|
||||
func codexCopyKind(cmd string) (kind, app string) {
|
||||
exe := cmd
|
||||
if i := strings.Index(cmd, " -"); i > 0 {
|
||||
exe = cmd[:i]
|
||||
}
|
||||
switch {
|
||||
case slices.ContainsFunc(strings.Fields(cmd), func(f string) bool {
|
||||
return f == "--managed-daemon" || strings.HasPrefix(f, "--managed-daemon=")
|
||||
}):
|
||||
return "daemon", ""
|
||||
case strings.Contains(exe, "/extensions/"):
|
||||
return "ide", ""
|
||||
case strings.Contains(exe, ".app/Contents/"):
|
||||
// the outermost bundle: ChatGPT.app's codex runs from a
|
||||
// CodexCLI.app inside it
|
||||
return "app", exe[:strings.Index(exe, ".app/")+len(".app")]
|
||||
}
|
||||
return "cli", ""
|
||||
}
|
||||
|
||||
// elapsed reads ps's etime, [[dd-]hh:]mm:ss.
|
||||
|
||||
+5
-1
@@ -196,6 +196,9 @@ type agentJSON struct {
|
||||
// started with (agent.Stale), for the line under its name
|
||||
Source string `json:"source,omitempty"`
|
||||
Stale int `json:"stale,omitempty"`
|
||||
// StaleCopies: those copies, each with what it is and when it
|
||||
// started, for the opened row to say which is left to reopen and how
|
||||
StaleCopies []agent.StaleCopy `json:"staleCopies,omitempty"`
|
||||
// Joined: connected with its own models still in its list (Codex
|
||||
// signed in with ChatGPT, agent.Agent.Join)
|
||||
Joined bool `json:"joined,omitempty"`
|
||||
@@ -1690,7 +1693,8 @@ func state() stateJSON {
|
||||
aj.Drift = a.Drift()
|
||||
aj.Wired = a.Wired()
|
||||
if aj.Wired {
|
||||
aj.Stale = a.Stale()
|
||||
aj.StaleCopies = a.StaleCopies()
|
||||
aj.Stale = len(aj.StaleCopies)
|
||||
aj.Joined = a.Joined != nil && a.Joined()
|
||||
} else {
|
||||
aj.Source = a.Source()
|
||||
|
||||
@@ -3187,6 +3187,8 @@ body.window .row + .row.agent.conn > .who { min-height: 41px; } /* the line abov
|
||||
:where(html[data-fonts]) .ag-code, :where(html[data-fonts]) .ag-diff code, :where(html[data-fonts]) .ag-path { font-style: var(--code-style, inherit); font-weight: var(--code-weight, inherit); font-stretch: var(--code-stretch, 100%); }
|
||||
.ag-code { padding: 1px 5px; border-radius: 4px; background: var(--pill); color: var(--fg); }
|
||||
.ag-warn { gap: 8px; font-size: 12px; }
|
||||
/* which copy is left on the old list: a sentence that wraps as one, under the warning's words */
|
||||
.ag-vl.ag-stale-copy { display: block; padding-left: 14px; font-size: 12px; line-height: 1.6; color: var(--fg-2); }
|
||||
.ag-hint { font-size: 12px; color: var(--muted); }
|
||||
.ag-tier { width: 70px; flex: none; font-size: 12px; color: var(--fg-2); }
|
||||
.ag-vl .field.ag-pick { width: 220px; justify-content: flex-start; }
|
||||
|
||||
@@ -999,6 +999,15 @@ function newModels(a) {
|
||||
}
|
||||
|
||||
const staleNow = (a) => a.wired && a.stale > 0 && staleSeen[a.id] !== a.stale;
|
||||
// a running copy left on the old list (agent.StaleCopy's kind): what it
|
||||
// is, since when, and how it is reopened — the words, and the command or
|
||||
// key in them as code
|
||||
const STALE_HOW = {
|
||||
app: ["The {agent} app, started {when}: closing its window keeps it running. Quit it with {cmd} and open it again.", "⌘Q"],
|
||||
ide: ["{agent} in an editor (VS Code, Cursor…), started {when}: reload that editor's window."],
|
||||
daemon: ["{agent}'s background app-server, started {when}: restart it with {cmd}.", "codex app-server daemon restart"],
|
||||
cli: ["A {agent} in a terminal, started {when}: quit it and start it again."],
|
||||
};
|
||||
|
||||
// connectLine: the dot and the words under an agent's name
|
||||
function connectLine(a, kind) {
|
||||
@@ -1263,6 +1272,17 @@ function connectPanel(a, { fields, fieldBtn }) {
|
||||
later.onclick = () => { staleSeen[a.id] = a.stale; renderAgents(); };
|
||||
w.append(later);
|
||||
parts.push(w);
|
||||
// which copy is left, and how it is reopened: a reopen of one kind
|
||||
// doesn't end another (the Codex app keeps running on macOS when its
|
||||
// window is closed; an editor's Codex and the CLI's daemon run on)
|
||||
for (const c of a.staleCopies || []) {
|
||||
const how = STALE_HOW[c.kind];
|
||||
if (!how) continue;
|
||||
const [pre, post] = t(how[0], { agent: a.name, when: ago(c.since) }).split("{cmd}");
|
||||
const l = line(pre, ...(how[1] ? [code(how[1]), post || ""] : []));
|
||||
l.classList.add("ag-stale-copy");
|
||||
parts.push(l);
|
||||
}
|
||||
}
|
||||
kv(t("In {agent}", { agent: a.name }), ...parts);
|
||||
} else if (menuFromList(a)) {
|
||||
|
||||
@@ -395,6 +395,10 @@ const I18N = {
|
||||
"Pick any model in {agent}'s {cmd}.": "在 {agent} 的 {cmd} 里选任何一个模型。",
|
||||
"1 running {agent} still has the old list": "1 个正在运行的 {agent} 还是旧列表",
|
||||
"{n} running {agent} still have the old list": "{n} 个正在运行的 {agent} 还是旧列表",
|
||||
"The {agent} app, started {when}: closing its window keeps it running. Quit it with {cmd} and open it again.": "{when}启动的 {agent} 应用:关掉窗口它仍在运行。用 {cmd} 退出后再打开。",
|
||||
"{agent} in an editor (VS Code, Cursor…), started {when}: reload that editor's window.": "{when}启动的编辑器(VS Code、Cursor…)里的 {agent}:重新加载那个编辑器的窗口。",
|
||||
"{agent}'s background app-server, started {when}: restart it with {cmd}.": "{when}启动的 {agent} 后台 app-server:用 {cmd} 重启它。",
|
||||
"A {agent} in a terminal, started {when}: quit it and start it again.": "{when}在终端里启动的 {agent}:退出后重新启动。",
|
||||
"Got it": "知道了",
|
||||
"In {agent}": "在 {agent} 里",
|
||||
"also for small background tasks": "也用于后台小任务",
|
||||
@@ -4123,6 +4127,10 @@ const I18N = {
|
||||
"Pick any model in {agent}'s {cmd}.": "在 {agent} 的 {cmd} 裡選任何一個模型。",
|
||||
"1 running {agent} still has the old list": "1 個正在執行的 {agent} 還是舊列表",
|
||||
"{n} running {agent} still have the old list": "{n} 個正在執行的 {agent} 還是舊列表",
|
||||
"The {agent} app, started {when}: closing its window keeps it running. Quit it with {cmd} and open it again.": "{when}啟動的 {agent} 應用:關掉視窗它仍在執行。用 {cmd} 結束後再打開。",
|
||||
"{agent} in an editor (VS Code, Cursor…), started {when}: reload that editor's window.": "{when}啟動的編輯器(VS Code、Cursor…)裡的 {agent}:重新載入那個編輯器的視窗。",
|
||||
"{agent}'s background app-server, started {when}: restart it with {cmd}.": "{when}啟動的 {agent} 背景 app-server:用 {cmd} 重新啟動它。",
|
||||
"A {agent} in a terminal, started {when}: quit it and start it again.": "{when}在終端機裡啟動的 {agent}:結束後重新啟動。",
|
||||
"Got it": "知道了",
|
||||
"In {agent}": "在 {agent} 裡",
|
||||
"also for small background tasks": "也用於後臺小任務",
|
||||
@@ -7816,6 +7824,10 @@ const I18N = {
|
||||
"Pick any model in {agent}'s {cmd}.": "{agent} の {cmd} で任意のモデルを選んでください。",
|
||||
"1 running {agent} still has the old list": "実行中の {agent} 1 件はまだ古い一覧のままです",
|
||||
"{n} running {agent} still have the old list": "実行中の {agent} {n} 件はまだ古い一覧のままです",
|
||||
"The {agent} app, started {when}: closing its window keeps it running. Quit it with {cmd} and open it again.": "{when}に起動した {agent} アプリ:ウィンドウを閉じても動き続けます。{cmd} で終了してから開き直してください。",
|
||||
"{agent} in an editor (VS Code, Cursor…), started {when}: reload that editor's window.": "{when}に起動したエディタ(VS Code、Cursor…)の {agent}:そのエディタのウィンドウを再読み込みしてください。",
|
||||
"{agent}'s background app-server, started {when}: restart it with {cmd}.": "{when}に起動した {agent} のバックグラウンド app-server:{cmd} で再起動してください。",
|
||||
"A {agent} in a terminal, started {when}: quit it and start it again.": "{when}にターミナルで起動した {agent}:終了して起動し直してください。",
|
||||
"Got it": "了解",
|
||||
"In {agent}": "{agent} 内",
|
||||
"also for small background tasks": "小さなバックグラウンドタスクにも使用",
|
||||
@@ -11527,6 +11539,10 @@ const I18N = {
|
||||
"Pick any model in {agent}'s {cmd}.": "Ein beliebiges Modell in {cmd} von {agent} wählen.",
|
||||
"1 running {agent} still has the old list": "1 laufende {agent}-Instanz hat noch die alte Liste",
|
||||
"{n} running {agent} still have the old list": "{n} laufende {agent}-Instanzen haben noch die alte Liste",
|
||||
"The {agent} app, started {when}: closing its window keeps it running. Quit it with {cmd} and open it again.": "Die {agent}-App, gestartet {when}: Wenn ihr Fenster geschlossen wird, läuft sie weiter. Mit {cmd} beenden und wieder öffnen.",
|
||||
"{agent} in an editor (VS Code, Cursor…), started {when}: reload that editor's window.": "{agent} in einem Editor (VS Code, Cursor…), gestartet {when}: das Fenster dieses Editors neu laden.",
|
||||
"{agent}'s background app-server, started {when}: restart it with {cmd}.": "Der Hintergrund-App-Server von {agent}, gestartet {when}: mit {cmd} neu starten.",
|
||||
"A {agent} in a terminal, started {when}: quit it and start it again.": "Ein {agent} im Terminal, gestartet {when}: beenden und neu starten.",
|
||||
"Got it": "Verstanden",
|
||||
"In {agent}": "In {agent}",
|
||||
"also for small background tasks": "auch für kleine Hintergrundaufgaben",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// miaopasi on Discord: the Codex row stayed "已接入 · 重开 Codex 后生效"
|
||||
// after Codex was restarted. On macOS closing the Codex app's window keeps
|
||||
// the app running, and an editor's Codex or the CLI's daemon runs on
|
||||
// through a reopened app. Opened, the row says which copy is left on the
|
||||
// old list, since when, and how that one is reopened: ⌘Q for the app, the
|
||||
// daemon's restart command. Every language, wide and narrow; "Got it"
|
||||
// takes it away. No backend, the API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const days = (n) => new Date(Date.now() - n * 86400e3).toISOString();
|
||||
const codex = {
|
||||
id: "codex", name: "Codex", icon: "generic", path: "/fixture/codex", wired: true,
|
||||
fields: [{ key: "model", label: "model", value: "gpt-6.1-sol", options: [{ value: "gpt-6.1-sol", label: "GPT-6.1 Sol", ref: "group/auto-gpt-6-1-sol", note: "Group · via magpie" }] }],
|
||||
stale: 2, staleCopies: [{ kind: "app", since: days(3) }, { kind: "daemon", since: days(1) }],
|
||||
};
|
||||
|
||||
function serve(lang) {
|
||||
return async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json({ agents: [codex], profiles: [], settings: { lang, theme: "light" } });
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname === "/api/groups") return json({ groups: [] });
|
||||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||||
if (url.pathname === "/api/agents/cli") return json({ agents: {}, pending: false });
|
||||
if (url.pathname === "/api/providers") return json({ providers: [], presets: [], excluded: [], gateway: { running: true, window: true } });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
await route.fulfill({ body: await fs.readFile(file), contentType });
|
||||
};
|
||||
}
|
||||
|
||||
const row = '.row.agent[data-id="codex"]';
|
||||
// as each engine's Intl says "3 days ago"
|
||||
const ago3 = { en: /3 days ago/, zh: /3 ?天前/, "zh-TW": /3 ?天前/, ja: /3 ?日前/, de: /vor 3 Tagen/ };
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh", "zh-TW", "ja", "de"]) {
|
||||
for (const width of [980, 440]) {
|
||||
test(`${engine} ${lang} ${width}px: the opened row says which Codex is left and how to reopen it`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
t.after(() => browser.close());
|
||||
const page = await browser.newPage({ viewport: { width, height: 800 } });
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang));
|
||||
await page.goto("http://magpie.test/?view=agents");
|
||||
await page.locator(`${row} .ag-conn`).waitFor();
|
||||
await page.locator(`${row} .ag-link`).click();
|
||||
const copies = page.locator(`${row} .ag-stale-copy`);
|
||||
await copies.first().waitFor();
|
||||
assert.equal(await copies.count(), 2);
|
||||
const [app, daemon] = await copies.allTextContents();
|
||||
assert.equal(await copies.nth(0).locator("code").textContent(), "⌘Q");
|
||||
assert.equal(await copies.nth(1).locator("code").textContent(), "codex app-server daemon restart");
|
||||
assert.match(app, ago3[lang], "the app's start");
|
||||
for (const s of [app, daemon]) {
|
||||
assert.ok(s.includes("Codex"), s);
|
||||
assert.doesNotMatch(s, /\{\w+\}/, "a placeholder left: " + s);
|
||||
}
|
||||
if (lang !== "en") assert.doesNotMatch(app, /closing its window/, "not translated: " + app);
|
||||
// nothing runs past the row
|
||||
const over = await page.evaluate((r) => [...document.querySelectorAll(r + " .ag-stale-copy, " + r + " .ag-stale-copy *")].filter((e) => e.scrollWidth > e.clientWidth + 1 || e.getBoundingClientRect().right > document.documentElement.clientWidth).map((e) => e.className || e.tagName), row);
|
||||
assert.deepEqual(over, []);
|
||||
// told: the lines go with the warning
|
||||
await page.locator(`${row} .ag-warn button`).click();
|
||||
await page.waitForFunction((r) => !document.querySelector(r + " .ag-stale-copy"), row);
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user