Progress: the reply's line is the 3.4.0 progress object, {{text}} · {{elapsed}}
ui/js/progress.js draws harness spec 3.4.0's `progress` (progress.md) with the spec's texts as the page's sentences: the thirteen phases, their variants and field words, the line with its clock in whole seconds (5s, 1m 05s, 1h 02m), none for stopped and failed, retrying counted down. A live reply takes `progress` from any stream frame that carries it and ticks once a second; with no progress (an older server, or null) it draws the status text as before. The sentences are the server's to translate: gen-i18n.py takes them from its catalogues once they are there (SERVER_PENDING), English until then, without failing. tests/ui: every case of the spec's conformance/progress.json (copied, 3.4.0), and a live stream's progress, clock, fallback and stopped line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+607
-2
No files matched your search
@@ -0,0 +1,333 @@
|
||||
{
|
||||
"about": "Progress (progress.md, spec 3.4.0): the text and the line every client draws for a progress object. `progress`: the object; `now`: the client's clock (Unix ms). Expect `text` — the phase's text from loop.json progress.texts, its fields filled (numbers written plainly: integers as they are, a rate with one decimal) — and `line` — `{{text}} · {{elapsed}}` with elapsed = now − since in whole seconds (5s, 1m 05s, 1h 02m), or the text alone for stopped and failed. retrying's `in` is counted down: in − floor((now − since) / 1000), never below 0. LLeMbas: services/progress.py; CLI: session/progress.ts.",
|
||||
"cases": [
|
||||
{
|
||||
"about": "Queued, nothing more known.",
|
||||
"progress": {
|
||||
"phase": "queued",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790405000,
|
||||
"expect": {
|
||||
"text": "Queued",
|
||||
"line": "Queued · 5s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Queued behind two turns.",
|
||||
"progress": {
|
||||
"phase": "queued",
|
||||
"since": 1762790400000,
|
||||
"ahead": 2
|
||||
},
|
||||
"now": 1762790465000,
|
||||
"expect": {
|
||||
"text": "Queued: 2 ahead",
|
||||
"line": "Queued: 2 ahead · 1m 05s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "A one-request server answering another request.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_model",
|
||||
"since": 1762790400000,
|
||||
"model": "Qwen 27B",
|
||||
"reason": "busy"
|
||||
},
|
||||
"now": 1762790412400,
|
||||
"expect": {
|
||||
"text": "Waiting for Qwen 27B: it serves one request at a time and is answering another",
|
||||
"line": "Waiting for Qwen 27B: it serves one request at a time and is answering another · 12s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Every slot taken.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_model",
|
||||
"since": 1762790400000,
|
||||
"model": "coder",
|
||||
"reason": "slots"
|
||||
},
|
||||
"now": 1762790401000,
|
||||
"expect": {
|
||||
"text": "Waiting for coder: all its slots are in use",
|
||||
"line": "Waiting for coder: all its slots are in use · 1s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Another model on the one-model server.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_model",
|
||||
"since": 1762790400000,
|
||||
"model": "coder",
|
||||
"reason": "group"
|
||||
},
|
||||
"now": 1762790400000,
|
||||
"expect": {
|
||||
"text": "Waiting for coder: its server holds another model now",
|
||||
"line": "Waiting for coder: its server holds another model now · 0s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Loading.",
|
||||
"progress": {
|
||||
"phase": "loading_model",
|
||||
"since": 1762790400000,
|
||||
"model": "Gemma 12B"
|
||||
},
|
||||
"now": 1762794125000,
|
||||
"expect": {
|
||||
"text": "Loading Gemma 12B",
|
||||
"line": "Loading Gemma 12B · 1h 02m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Reading the prompt: the percentage is floored.",
|
||||
"progress": {
|
||||
"phase": "reading_prompt",
|
||||
"since": 1762790400000,
|
||||
"done": 1999,
|
||||
"total": 3000
|
||||
},
|
||||
"now": 1762790402000,
|
||||
"expect": {
|
||||
"text": "Reading the prompt: 1999 / 3000 tokens (66%)",
|
||||
"line": "Reading the prompt: 1999 / 3000 tokens (66%) · 2s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Reading the prompt with no counts from the server.",
|
||||
"progress": {
|
||||
"phase": "reading_prompt",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790402000,
|
||||
"expect": {
|
||||
"text": "Reading the prompt",
|
||||
"line": "Reading the prompt · 2s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Thinking.",
|
||||
"progress": {
|
||||
"phase": "thinking",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790459999,
|
||||
"expect": {
|
||||
"text": "Thinking",
|
||||
"line": "Thinking · 59s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Writing, the rate with one decimal.",
|
||||
"progress": {
|
||||
"phase": "writing",
|
||||
"since": 1762790400000,
|
||||
"rate": 12.46
|
||||
},
|
||||
"now": 1762790404000,
|
||||
"expect": {
|
||||
"text": "Writing: 12.5 tok/s",
|
||||
"line": "Writing: 12.5 tok/s · 4s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Writing before a rate is measured.",
|
||||
"progress": {
|
||||
"phase": "writing",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790400400,
|
||||
"expect": {
|
||||
"text": "Writing",
|
||||
"line": "Writing · 0s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "A tool with what it does.",
|
||||
"progress": {
|
||||
"phase": "running_tool",
|
||||
"since": 1762790400000,
|
||||
"tool": "bash",
|
||||
"what": "bun test"
|
||||
},
|
||||
"now": 1762790461000,
|
||||
"expect": {
|
||||
"text": "Running bash: bun test",
|
||||
"line": "Running bash: bun test · 1m 01s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "A tool without a title.",
|
||||
"progress": {
|
||||
"phase": "running_tool",
|
||||
"since": 1762790400000,
|
||||
"tool": "recall"
|
||||
},
|
||||
"now": 1762790401000,
|
||||
"expect": {
|
||||
"text": "Running recall",
|
||||
"line": "Running recall · 1s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "An approval, naming the tool.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_person",
|
||||
"since": 1762790400000,
|
||||
"what": "approval",
|
||||
"tool": "bash"
|
||||
},
|
||||
"now": 1762790430000,
|
||||
"expect": {
|
||||
"text": "Waiting for you: allow bash?",
|
||||
"line": "Waiting for you: allow bash? · 30s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "An approval with no tool named.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_person",
|
||||
"since": 1762790400000,
|
||||
"what": "approval"
|
||||
},
|
||||
"now": 1762790400000,
|
||||
"expect": {
|
||||
"text": "Waiting for you: an approval",
|
||||
"line": "Waiting for you: an approval · 0s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "A question.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_person",
|
||||
"since": 1762790400000,
|
||||
"what": "question"
|
||||
},
|
||||
"now": 1762790403000,
|
||||
"expect": {
|
||||
"text": "Waiting for you: a question",
|
||||
"line": "Waiting for you: a question · 3s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "The plan.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_person",
|
||||
"since": 1762790400000,
|
||||
"what": "plan"
|
||||
},
|
||||
"now": 1762790403000,
|
||||
"expect": {
|
||||
"text": "Waiting for you: the plan",
|
||||
"line": "Waiting for you: the plan · 3s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Subagents.",
|
||||
"progress": {
|
||||
"phase": "waiting_for_subagents",
|
||||
"since": 1762790400000,
|
||||
"running": 3
|
||||
},
|
||||
"now": 1762790490000,
|
||||
"expect": {
|
||||
"text": "Waiting for subagents: 3 running",
|
||||
"line": "Waiting for subagents: 3 running · 1m 30s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Compacting.",
|
||||
"progress": {
|
||||
"phase": "compacting",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790407000,
|
||||
"expect": {
|
||||
"text": "Compacting the conversation",
|
||||
"line": "Compacting the conversation · 7s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Retrying: `in` counts down from since.",
|
||||
"progress": {
|
||||
"phase": "retrying",
|
||||
"since": 1762790400000,
|
||||
"reason": "502 Bad Gateway",
|
||||
"in": 10,
|
||||
"attempt": 2
|
||||
},
|
||||
"now": 1762790403000,
|
||||
"expect": {
|
||||
"text": "Retrying in 7 s: 502 Bad Gateway",
|
||||
"line": "Retrying in 7 s: 502 Bad Gateway · 3s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Retrying: never below 0.",
|
||||
"progress": {
|
||||
"phase": "retrying",
|
||||
"since": 1762790400000,
|
||||
"reason": "overloaded",
|
||||
"in": 2
|
||||
},
|
||||
"now": 1762790405000,
|
||||
"expect": {
|
||||
"text": "Retrying in 0 s: overloaded",
|
||||
"line": "Retrying in 0 s: overloaded · 5s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Stopped draws no elapsed time.",
|
||||
"progress": {
|
||||
"phase": "stopped",
|
||||
"since": 1762790400000
|
||||
},
|
||||
"now": 1762790405000,
|
||||
"expect": {
|
||||
"text": "Stopped",
|
||||
"line": "Stopped"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Stopped by the person.",
|
||||
"progress": {
|
||||
"phase": "stopped",
|
||||
"since": 1762790400000,
|
||||
"reason": "person"
|
||||
},
|
||||
"now": 1762790405000,
|
||||
"expect": {
|
||||
"text": "Stopped: by you",
|
||||
"line": "Stopped: by you"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Stopped at the step limit.",
|
||||
"progress": {
|
||||
"phase": "stopped",
|
||||
"since": 1762790400000,
|
||||
"reason": "steps"
|
||||
},
|
||||
"now": 1762790400000,
|
||||
"expect": {
|
||||
"text": "Stopped: step limit",
|
||||
"line": "Stopped: step limit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"about": "Failed.",
|
||||
"progress": {
|
||||
"phase": "failed",
|
||||
"since": 1762790400000,
|
||||
"reason": "connection refused"
|
||||
},
|
||||
"now": 1762790405000,
|
||||
"expect": {
|
||||
"text": "Failed: connection refused",
|
||||
"line": "Failed: connection refused"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -615,6 +615,49 @@ test("B3: at 1280 px the terminal sits beside the chat, which keeps its column",
|
||||
assert.deepEqual(p.errors, [])
|
||||
})
|
||||
|
||||
// --- B1: the progress line ---------------------------------------------------------------------
|
||||
//
|
||||
// Harness spec 3.4.0 (harness/progress.md in the LLeMbas CLI): one `progress` object, drawn by
|
||||
// every client as `{{text}} · {{elapsed}}`. tests/ui/conformance/progress.json is a copy of the
|
||||
// spec's conformance/progress.json (3.4.0); every case must come out word for word.
|
||||
|
||||
test("B1: every conformance case of the spec draws the same text and line", async () => {
|
||||
const { cases } = JSON.parse(readFileSync(join(HERE, "conformance", "progress.json"), "utf8"))
|
||||
const p = await page(b3())
|
||||
const got = await p.evaluate(async (cs) => {
|
||||
const m = await import("/js/progress.js")
|
||||
return cs.map((c) => ({ text: m.progressText(c.progress, c.now), line: m.progressLine(c.progress, c.now) }))
|
||||
}, cases)
|
||||
cases.forEach((c, i) => assert.deepEqual(got[i], c.expect, c.about))
|
||||
assert.deepEqual(p.errors, [])
|
||||
})
|
||||
|
||||
test("B1: a reply being written draws the stream's progress, its clock moving, and the status text without one", async () => {
|
||||
const p = await page(agentChatOpen([["status", { text: "Writing…" }]]))
|
||||
await openChatRow(p, "chat2")
|
||||
await p.waitForSelector(".msg.is-live .msg__status")
|
||||
await p.evaluate((path) => {
|
||||
window.__mock.frame(path, "status", { text: "Running bake --slow", progress: { phase: "running_tool", since: Date.now() - 65000, tool: "bash", what: "bake --slow" } })
|
||||
}, STREAM2)
|
||||
const line = () => p.textContent(".msg.is-live .msg__status")
|
||||
assert.equal(await line(), "Running bash: bake --slow · 1m 05s")
|
||||
assert.equal(await p.getAttribute(".msg.is-live .msg__status", "data-phase"), "running_tool")
|
||||
await p.waitForFunction(() => document.querySelector(".msg.is-live .msg__status").textContent.endsWith("1m 06s"), null, { timeout: 2500 })
|
||||
// A data-only frame with progress (the ask frame carries it too): the line follows.
|
||||
await p.evaluate((path) => window.__mock.frame(path, "ask", { cards: [], progress: { phase: "waiting_for_person", since: Date.now(), what: "approval", tool: "bash" } }), STREAM2)
|
||||
assert.equal(await line(), "Waiting for you: allow bash? · 0s")
|
||||
// Nothing running: `progress: null`, and the status text is drawn again.
|
||||
await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Writing…", progress: null }), STREAM2)
|
||||
assert.equal(await line(), "Writing…")
|
||||
// An older server sends no progress at all: its status text, as before.
|
||||
await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Running bake --slow" }), STREAM2)
|
||||
assert.equal(await line(), "Running bake --slow")
|
||||
// Stopped and failed have no clock.
|
||||
await p.evaluate((path) => window.__mock.frame(path, "status", { text: "Stopped.", progress: { phase: "stopped", since: Date.now() - 4000, reason: "person" } }), STREAM2)
|
||||
assert.equal(await line(), "Stopped: by you")
|
||||
assert.deepEqual(p.errors, [])
|
||||
})
|
||||
|
||||
// --- Run ----------------------------------------------------------------------------------------
|
||||
|
||||
const only = process.argv[2] || ""
|
||||
|
||||
Reference in new issue
Block a user