control board: show the model each seat is running (#1503)
Jason asked for the running model (sonnet, opus, gpt-6-astra) on the board. readSession keeps model and provider from the log's latest model_change entry or assistant turn, whichever is later, so a /model switch shows on the next scan; scanAgent exposes both; the page shows the model under the agent name with the provider in the hover and a Model detail row. Blank when the log names none. Live check on a scratch board against the real data root: all 42 rows carried a model. Board 91/91. Sonnet review caught a double-escaped hover title; fixed. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -2122,3 +2122,14 @@ gone pid marks the record stale: kept on the Registered line with
|
|||||||
index, stale count in the CLI summary. Fleet launchers marked not planned
|
index, stale count in the CLI summary. Fleet launchers marked not planned
|
||||||
per Jason. Tests: board 90/90, seat 15/15, launch scripts 5/5, real data
|
per Jason. Tests: board 90/90, seat 15/15, launch scripts 5/5, real data
|
||||||
root untouched by the run. The three fixture records were deleted by hand.
|
root untouched by the run. The three fixture records were deleted by hand.
|
||||||
|
|
||||||
|
## 2026-09-12 — Control board: model per row (#1503)
|
||||||
|
|
||||||
|
Before: the board did not say which model a seat was running. After:
|
||||||
|
`readSession` keeps `model` and `provider` from the log's latest
|
||||||
|
`model_change` entry or assistant turn, whichever is later; the record
|
||||||
|
carries both; the page shows the model under the agent name with the
|
||||||
|
provider in the hover, and a Model detail row. Blank when the log names
|
||||||
|
none. Live check against the real data root on a scratch board: all 42
|
||||||
|
rows carried a model. Board suite 91/91 (was 90). Sonnet review recorded
|
||||||
|
in the plan page entry.
|
||||||
|
|||||||
@@ -222,3 +222,4 @@ are never rewritten or removed; corrections are new entries.
|
|||||||
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, Gate A fix (#1503), asked by the professor session on Jason's behalf: per-row Task (first user message), Active project (nearest git checkout above the workspace) and Workspace (live tmux pane path, else session cwd), "unknown" when not derivable; missing launcher signals logged on the plan page. Fixture test per field (79/79); live check on a scratch server; sonnet review APPROVED (no findings).
|
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, Gate A fix (#1503), asked by the professor session on Jason's behalf: per-row Task (first user message), Active project (nearest git checkout above the workspace) and Workspace (live tmux pane path, else session cwd), "unknown" when not derivable; missing launcher signals logged on the plan page. Fixture test per field (79/79); live check on a scratch server; sonnet review APPROVED (no findings).
|
||||||
- 2026-09-12 — darkwing (Claude Code, coordinator; formerly jarvis) — Seat registration (#1504), Jason's go on the professor session's brief: new `packages/seat` and `scripts/mosaic` (`launch <seat>` registers then execs the launch script unchanged; `seat task` edits the task); board reads `<dataRoot>/seats/<layout>/<seat>/registration.json` and lets registered task/project/workspace override derived values with a source tag; the four repo launch scripts register themselves; fleet one-liner written for orch-01. Tests: seat 15, board 79 → 89, launch scripts 5, registry 69, config 24. Sonnet review found a seat-name collision across layouts; fixed by keying on layout, second pass APPROVED. Live check on a scratch server against a temp data root. Also found Jason's launch refused on the Pi pin (node_modules 0.84.4 vs pin 0.85.1 since 557aba0f); `npm ci` fixed it, the refusal now names both versions; the pin stays unless Jason decides otherwise.
|
- 2026-09-12 — darkwing (Claude Code, coordinator; formerly jarvis) — Seat registration (#1504), Jason's go on the professor session's brief: new `packages/seat` and `scripts/mosaic` (`launch <seat>` registers then execs the launch script unchanged; `seat task` edits the task); board reads `<dataRoot>/seats/<layout>/<seat>/registration.json` and lets registered task/project/workspace override derived values with a source tag; the four repo launch scripts register themselves; fleet one-liner written for orch-01. Tests: seat 15, board 79 → 89, launch scripts 5, registry 69, config 24. Sonnet review found a seat-name collision across layouts; fixed by keying on layout, second pass APPROVED. Live check on a scratch server against a temp data root. Also found Jason's launch refused on the Pi pin (node_modules 0.84.4 vs pin 0.85.1 since 557aba0f); `npm ci` fixed it, the refusal now names both versions; the pin stays unless Jason decides otherwise.
|
||||||
- 2026-09-12 — darkwing (Claude Code, coordinator) — #1504 follow-up on two defects the professor session reported and this session verified: the darkwing launch test leaked fixture registrations into the real data root (flock spawn without the fixture config; fixed and guarded so a leak fails), and a refused launch left a dead-pid record the board honoured (scanner now probes the pid; stale records are shown but do not override). Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15, launch scripts 5/5. Committed and pushed to refactor.
|
- 2026-09-12 — darkwing (Claude Code, coordinator) — #1504 follow-up on two defects the professor session reported and this session verified: the darkwing launch test leaked fixture registrations into the real data root (flock spawn without the fixture config; fixed and guarded so a leak fails), and a refused launch left a dead-pid record the board honoured (scanner now probes the pid; stale records are shown but do not override). Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15, launch scripts 5/5. Committed and pushed to refactor.
|
||||||
|
- 2026-09-12 — darkwing (Claude Code, coordinator) — Control board: model per row (#1503) on Jason's ask: readSession tracks model/provider from the latest model_change entry or assistant turn; page shows the model under the agent name and a Model detail row. Live check on a scratch board: all 42 real rows named a model. Board 91/91. Committed and pushed to refactor.
|
||||||
|
|||||||
@@ -347,4 +347,6 @@ log names no model; rocko has no pi log and stays off the board. Checked
|
|||||||
on a scratch board against the real data root: all 42 rows carried a
|
on a scratch board against the real data root: all 42 rows carried a
|
||||||
model (darkwing and sage `gpt-6-astra`, filbert `glm-5.3`, fleet rows a mix
|
model (darkwing and sage `gpt-6-astra`, filbert `glm-5.3`, fleet rows a mix
|
||||||
of `gpt-5.6-*` and `glm-5.3`). Board suite 91/91. Jason's board on 7331
|
of `gpt-5.6-*` and `glm-5.3`). Board suite 91/91. Jason's board on 7331
|
||||||
needs a restart to pick this up.
|
needs a restart to pick this up. Sonnet review found the model hover title
|
||||||
|
escaped an already escaped string (double encoding for names with `&` or
|
||||||
|
`<`); fixed before commit, then APPROVED on that basis.
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ tmux, else shown as the word "unknown", never guessed:
|
|||||||
tmux could not be asked, the `cwd` from the session log. The record says
|
tmux could not be asked, the `cwd` from the session log. The record says
|
||||||
which one it used (`workspaceSource`: `tmux-pane` or `session-cwd`).
|
which one it used (`workspaceSource`: `tmux-pane` or `session-cwd`).
|
||||||
|
|
||||||
|
- **Model** (under the agent name, and a detail row with the provider) —
|
||||||
|
the model the seat is running, from the log's latest `model_change` entry
|
||||||
|
or assistant turn, whichever is later, so a `/model` switch mid-session
|
||||||
|
shows up on the next scan. Blank until the log names one; a seat with no
|
||||||
|
pi log (rocko) has none.
|
||||||
|
|
||||||
A seat started through `scripts/mosaic launch <seat>` (see
|
A seat started through `scripts/mosaic launch <seat>` (see
|
||||||
`packages/seat/README.md`, #1504) has a registration at
|
`packages/seat/README.md`, #1504) has a registration at
|
||||||
`<dataRoot>/seats/<layout>/<seat>/registration.json`. The scan reads every one of
|
`<dataRoot>/seats/<layout>/<seat>/registration.json`. The scan reads every one of
|
||||||
@@ -166,6 +172,8 @@ values), the scan refuses rather than silently dropping every mark.
|
|||||||
"registered": null,
|
"registered": null,
|
||||||
"lastActivity": "2026-09-12T15:04:33.000Z",
|
"lastActivity": "2026-09-12T15:04:33.000Z",
|
||||||
"ageSeconds": 42,
|
"ageSeconds": 42,
|
||||||
|
"model": "gpt-6-astra",
|
||||||
|
"provider": "openai-codex",
|
||||||
"lastAssistantText": "Ready for the next step whenever you are.",
|
"lastAssistantText": "Ready for the next step whenever you are.",
|
||||||
"lastError": null,
|
"lastError": null,
|
||||||
"skippedLines": 0,
|
"skippedLines": 0,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
.seen-tag{font-size:.72rem;color:var(--muted);margin-left:6px;vertical-align:middle}
|
.seen-tag{font-size:.72rem;color:var(--muted);margin-left:6px;vertical-align:middle}
|
||||||
.msg-error{color:var(--danger)}
|
.msg-error{color:var(--danger)}
|
||||||
.msg-text,.msg-error{display:block;max-width:36ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.msg-text,.msg-error{display:block;max-width:36ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.model{display:block;color:var(--muted);font-size:.75em;white-space:nowrap}
|
||||||
.task-text{display:block;max-width:28ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.task-text{display:block;max-width:28ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.unknown{color:var(--muted);font-style:italic}
|
.unknown{color:var(--muted);font-style:italic}
|
||||||
.source{color:var(--muted);font-size:.75em;margin-left:.35em;white-space:nowrap}
|
.source{color:var(--muted);font-size:.75em;margin-left:.35em;white-space:nowrap}
|
||||||
@@ -189,6 +190,11 @@
|
|||||||
function sourceLabel(source) { return SOURCE_LABEL[source] || source; }
|
function sourceLabel(source) { return SOURCE_LABEL[source] || source; }
|
||||||
function sourceTag(source) { return source ? '<span class="source" title="source: ' + esc(source) + '">' + esc(sourceLabel(source)) + "</span>" : ""; }
|
function sourceTag(source) { return source ? '<span class="source" title="source: ' + esc(source) + '">' + esc(sourceLabel(source)) + "</span>" : ""; }
|
||||||
function fromSource(source) { return source ? " (from " + esc(sourceLabel(source)) + ")" : ""; }
|
function fromSource(source) { return source ? " (from " + esc(sourceLabel(source)) + ")" : ""; }
|
||||||
|
// The model a seat is running, e.g. "gpt-6-astra" with the provider in the
|
||||||
|
// hover title. modelText() already escapes, so the title takes it as is.
|
||||||
|
// Empty when the log has not named one yet.
|
||||||
|
function modelTag(rec) { return rec.model ? '<span class="model" title="' + modelText(rec) + '">' + esc(rec.model) + "</span>" : ""; }
|
||||||
|
function modelText(rec) { return rec.model ? (rec.provider ? esc(rec.provider) + "/" : "") + esc(rec.model) : "unknown"; }
|
||||||
function registeredText(reg) {
|
function registeredText(reg) {
|
||||||
if (!reg) return "no (not started through mosaic launch)";
|
if (!reg) return "no (not started through mosaic launch)";
|
||||||
var parts = ["started " + esc(reg.startedAt || "—")];
|
var parts = ["started " + esc(reg.startedAt || "—")];
|
||||||
@@ -222,7 +228,7 @@
|
|||||||
var main =
|
var main =
|
||||||
'<tr class="' + cls + '">' +
|
'<tr class="' + cls + '">' +
|
||||||
projectCell +
|
projectCell +
|
||||||
'<td><button type="button" class="row-toggle" data-idx="' + idx + '" data-key="' + esc(key) + '" aria-expanded="' + (open ? "true" : "false") + '" aria-controls="detail-' + idx + '">' + esc(rec.agent) + "</button></td>" +
|
'<td><button type="button" class="row-toggle" data-idx="' + idx + '" data-key="' + esc(key) + '" aria-expanded="' + (open ? "true" : "false") + '" aria-controls="detail-' + idx + '">' + esc(rec.agent) + "</button>" + modelTag(rec) + "</td>" +
|
||||||
"<td>" + badge(rec.state) + seenControl(rec) + "</td>" +
|
"<td>" + badge(rec.state) + seenControl(rec) + "</td>" +
|
||||||
"<td>" + esc(humanAge(rec.ageSeconds)) + "</td>" +
|
"<td>" + esc(humanAge(rec.ageSeconds)) + "</td>" +
|
||||||
"<td>" + task + "</td>" +
|
"<td>" + task + "</td>" +
|
||||||
@@ -241,6 +247,7 @@
|
|||||||
"<dt>Task</dt><dd>" + (rec.task ? esc(rec.task) : "unknown") + fromSource(rec.taskSource) + "</dd>" +
|
"<dt>Task</dt><dd>" + (rec.task ? esc(rec.task) : "unknown") + fromSource(rec.taskSource) + "</dd>" +
|
||||||
"<dt>Active project</dt><dd>" + (rec.activeProject ? esc(rec.activeProject) : "unknown") + fromSource(rec.activeProjectSource) + "</dd>" +
|
"<dt>Active project</dt><dd>" + (rec.activeProject ? esc(rec.activeProject) : "unknown") + fromSource(rec.activeProjectSource) + "</dd>" +
|
||||||
"<dt>Workspace</dt><dd>" + (rec.workspace ? esc(rec.workspace) : "unknown") + fromSource(rec.workspaceSource) + "</dd>" +
|
"<dt>Workspace</dt><dd>" + (rec.workspace ? esc(rec.workspace) : "unknown") + fromSource(rec.workspaceSource) + "</dd>" +
|
||||||
|
"<dt>Model</dt><dd>" + modelText(rec) + "</dd>" +
|
||||||
"<dt>Registered</dt><dd>" + registeredText(rec.registered) + "</dd>" +
|
"<dt>Registered</dt><dd>" + registeredText(rec.registered) + "</dd>" +
|
||||||
"<dt>Session cwd</dt><dd>" + esc(rec.cwd || "—") + "</dd>" +
|
"<dt>Session cwd</dt><dd>" + esc(rec.cwd || "—") + "</dd>" +
|
||||||
"<dt>Tmux session</dt><dd>" + tmux + "</dd>" +
|
"<dt>Tmux session</dt><dd>" + tmux + "</dd>" +
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export function readSession(file) {
|
|||||||
const lines = readFileSync(file, "utf8").split("\n");
|
const lines = readFileSync(file, "utf8").split("\n");
|
||||||
let sessionId = null, cwd = null, lastTimestamp = null, lastMessage = null, lastAssistantText = null, lastError = null;
|
let sessionId = null, cwd = null, lastTimestamp = null, lastMessage = null, lastAssistantText = null, lastError = null;
|
||||||
let firstUserText = null;
|
let firstUserText = null;
|
||||||
|
let model = null, provider = null;
|
||||||
let skippedLines = 0;
|
let skippedLines = 0;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
@@ -85,8 +86,18 @@ export function readSession(file) {
|
|||||||
if (entry.type === "session") {
|
if (entry.type === "session") {
|
||||||
sessionId = entry.id ?? sessionId;
|
sessionId = entry.id ?? sessionId;
|
||||||
cwd = entry.cwd ?? cwd;
|
cwd = entry.cwd ?? cwd;
|
||||||
|
} else if (entry.type === "model_change") {
|
||||||
|
// pi logs a model switch (/model, or the launch default) as its own
|
||||||
|
// entry: { provider, modelId }. The latest one is the model in use.
|
||||||
|
if (typeof entry.modelId === "string" && entry.modelId) model = entry.modelId;
|
||||||
|
if (typeof entry.provider === "string" && entry.provider) provider = entry.provider;
|
||||||
} else if (entry.type === "message" && entry.message) {
|
} else if (entry.type === "message" && entry.message) {
|
||||||
lastMessage = entry.message;
|
lastMessage = entry.message;
|
||||||
|
if (entry.message.role === "assistant") {
|
||||||
|
// Each assistant turn also names the model that produced it.
|
||||||
|
if (typeof entry.message.model === "string" && entry.message.model) model = entry.message.model;
|
||||||
|
if (typeof entry.message.provider === "string" && entry.message.provider) provider = entry.message.provider;
|
||||||
|
}
|
||||||
if (entry.message.role === "user" && firstUserText === null) {
|
if (entry.message.role === "user" && firstUserText === null) {
|
||||||
const text = userText(entry.message.content);
|
const text = userText(entry.message.content);
|
||||||
if (text.trim()) firstUserText = collapse(text);
|
if (text.trim()) firstUserText = collapse(text);
|
||||||
@@ -98,7 +109,7 @@ export function readSession(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { file, sessionId, cwd, lastTimestamp, lastMessage, lastAssistantText, lastError, firstUserText, skippedLines };
|
return { file, sessionId, cwd, lastTimestamp, lastMessage, lastAssistantText, lastError, firstUserText, model, provider, skippedLines };
|
||||||
}
|
}
|
||||||
|
|
||||||
// A user message's text: pi writes either a plain string or a list of blocks.
|
// A user message's text: pi writes either a plain string or a list of blocks.
|
||||||
@@ -341,6 +352,11 @@ export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(),
|
|||||||
sessionFile: file,
|
sessionFile: file,
|
||||||
sessionId: session?.sessionId ?? null,
|
sessionId: session?.sessionId ?? null,
|
||||||
cwd,
|
cwd,
|
||||||
|
// The model the seat is running, from the log's latest model_change entry
|
||||||
|
// or assistant turn, whichever came last. null when the log has neither
|
||||||
|
// (a seat with no log, or one that has not answered yet).
|
||||||
|
model: session?.model ?? null,
|
||||||
|
provider: session?.provider ?? null,
|
||||||
task,
|
task,
|
||||||
taskSource,
|
taskSource,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ function writeFile(path, content) {
|
|||||||
function sessionLine({ id, timestamp, cwd }) {
|
function sessionLine({ id, timestamp, cwd }) {
|
||||||
return JSON.stringify({ type: "session", id, timestamp, cwd });
|
return JSON.stringify({ type: "session", id, timestamp, cwd });
|
||||||
}
|
}
|
||||||
function messageLine({ timestamp, role, stopReason, texts, blocks }) {
|
function messageLine({ timestamp, role, stopReason, texts, blocks, model, provider }) {
|
||||||
const message = { role };
|
const message = { role };
|
||||||
if (stopReason !== undefined) message.stopReason = stopReason;
|
if (stopReason !== undefined) message.stopReason = stopReason;
|
||||||
|
if (model !== undefined) message.model = model;
|
||||||
|
if (provider !== undefined) message.provider = provider;
|
||||||
if (texts) message.content = texts.map((text) => ({ type: "text", text }));
|
if (texts) message.content = texts.map((text) => ({ type: "text", text }));
|
||||||
if (blocks) message.content = (message.content || []).concat(blocks);
|
if (blocks) message.content = (message.content || []).concat(blocks);
|
||||||
return JSON.stringify({ type: "message", timestamp, message });
|
return JSON.stringify({ type: "message", timestamp, message });
|
||||||
@@ -190,6 +192,45 @@ test("readSession: extracts fields, collapses/truncates text, counts a truncated
|
|||||||
assert.equal(result.skippedLines, 1);
|
assert.equal(result.skippedLines, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them", () => {
|
||||||
|
const root = makeRoot();
|
||||||
|
const modelChange = (timestamp, provider, modelId) => JSON.stringify({ type: "model_change", id: "m", parentId: null, timestamp, provider, modelId });
|
||||||
|
const switchedDir = join(root, "switched");
|
||||||
|
writeSessionFile(switchedDir, "model.jsonl", [
|
||||||
|
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||||
|
modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"),
|
||||||
|
messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }),
|
||||||
|
messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "glm-5.3-flash", provider: "zai" }),
|
||||||
|
modelChange("2026-09-12T16:08:52.233Z", "openai-codex", "gpt-6-astra"),
|
||||||
|
]);
|
||||||
|
const switched = readSession(join(switchedDir, "model.jsonl"));
|
||||||
|
assert.equal(switched.model, "gpt-6-astra", "a /model switch after the last answer wins");
|
||||||
|
assert.equal(switched.provider, "openai-codex");
|
||||||
|
|
||||||
|
const answered = writeSessionFile(root, "answered.jsonl", [
|
||||||
|
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||||
|
modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"),
|
||||||
|
messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "claude-sonnet-5", provider: "anthropic" }),
|
||||||
|
messageLine({ timestamp: "2026-09-12T16:07:06Z", role: "user", texts: ["and?"] }),
|
||||||
|
]);
|
||||||
|
assert.deepEqual([readSession(answered).model, readSession(answered).provider], ["claude-sonnet-5", "anthropic"], "the assistant turn names the model that produced it");
|
||||||
|
|
||||||
|
const noneDir = join(root, "none");
|
||||||
|
writeSessionFile(noneDir, "none.jsonl", [
|
||||||
|
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||||
|
messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }),
|
||||||
|
]);
|
||||||
|
assert.equal(readSession(join(noneDir, "none.jsonl")).model, null);
|
||||||
|
assert.equal(readSession(join(noneDir, "none.jsonl")).provider, null);
|
||||||
|
|
||||||
|
const rec = scanAgent({ agent: "a", project: "p", sessionsDir: switchedDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW });
|
||||||
|
assert.equal(rec.model, "gpt-6-astra", "scanAgent carries model through from the newest log");
|
||||||
|
assert.equal(rec.provider, "openai-codex");
|
||||||
|
const bare = scanAgent({ agent: "a", project: "p", sessionsDir: noneDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW });
|
||||||
|
assert.equal(bare.model, null);
|
||||||
|
assert.equal(bare.provider, null);
|
||||||
|
});
|
||||||
|
|
||||||
test("readSession: lastError carries the assistant errorMessage only when the last assistant turn errored", () => {
|
test("readSession: lastError carries the assistant errorMessage only when the last assistant turn errored", () => {
|
||||||
const root = makeRoot();
|
const root = makeRoot();
|
||||||
const errored = JSON.stringify({
|
const errored = JSON.stringify({
|
||||||
|
|||||||
@@ -655,6 +655,15 @@ test("page.html: task and active project cells show their source via sourceTag()
|
|||||||
"the detail list has a Registered row built by registeredText()",
|
"the detail list has a Registered row built by registeredText()",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.match(rowPair[0], /esc\(rec\.agent\) \+ "<\/button>" \+ modelTag\(rec\)/, "the agent cell shows the model under the name");
|
||||||
|
assert.match(rowPair[0], /<dt>Model<\/dt><dd>" \+ modelText\(rec\) \+ "<\/dd>/, "the detail list has a Model row");
|
||||||
|
for (const name of ["modelTag", "modelText"]) {
|
||||||
|
const line = html.split("\n").find((l) => l.includes(`function ${name}(rec)`));
|
||||||
|
assert.ok(line, `${name}() must exist in page.html`);
|
||||||
|
assert.match(line, /esc\(rec\.model\)/, `${name}() escapes the model`);
|
||||||
|
if (name === "modelText") assert.match(line, /esc\(rec\.provider\)/, "modelText() escapes the provider");
|
||||||
|
}
|
||||||
|
|
||||||
const sourceLabelMatch = html.match(/var SOURCE_LABEL = (\{.*\});/);
|
const sourceLabelMatch = html.match(/var SOURCE_LABEL = (\{.*\});/);
|
||||||
assert.ok(sourceLabelMatch, "SOURCE_LABEL must exist in page.html");
|
assert.ok(sourceLabelMatch, "SOURCE_LABEL must exist in page.html");
|
||||||
const sourceLabel = JSON.parse(sourceLabelMatch[1]);
|
const sourceLabel = JSON.parse(sourceLabelMatch[1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user