Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions console/tests/time.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ before(async () => {
for (let number = 1; number <= 7; number++) {
const live = number === 1;
const record = {
version: 1, kind: 'Run', name: `run-${number}`, repository: 'acme/widgets', hostname: 'fixture',
version: 1, kind: number === 2 ? 'Spec run' : 'Run', name: `run-${number}`, repository: 'acme/widgets', hostname: 'fixture',
issue: { number, url: `https://github.com/acme/widgets/issues/${number}`, title: `Run ${number} <script>unsafe()</script>` },
process_id: live ? process.pid : 0, process_start: live ? processStart : 'gone', harness: 'codex',
started_at: time(number === 7 ? 1500 : 180 - number * 10),
Expand All @@ -38,9 +38,21 @@ before(async () => {
],
...(number === 6 ? { parent_record: join(folder, '2-fixture.command.json') } : {})
};
if (live) {
const dispatched = { ...record };
record.kind = 'Pickup run';
record.name = 'pickup-fixture';
record.stages = [{ name: 'dispatched Run', began_at: time(170), session_log: null }];
record.dispatched = [dispatched];
}
await writeFile(join(folder, `${number}-fixture.command.json`), JSON.stringify(record));
await writeFile(join(folder, `${number}-fixture.log`), `Command ${number}\n`);
}
const security = JSON.parse(await readFile(join(folder, '3-fixture.command.json'), 'utf8'));
security.kind = 'Security run';
security.name = 'security-fixture';
security.issue = null;
await writeFile(join(folder, '10-fixture.command.json'), JSON.stringify(security));
// A finished Run in an alphabetically earlier repository must not precede
// this repository's live Run, or reorder the finished columns by repository.
const otherFolder = join(home, '.thirdshift/logs/aaa/first/commands/issue');
Expand Down Expand Up @@ -75,7 +87,7 @@ test('Lanes scales every window, nests child Runs and fits 390 px without CSP vi
for (const hours of [3.5, 12, 24]) {
await page.goto(`${origin}/?page=lanes&hours=${hours}`);
assert.equal(await page.locator('[name="hours"]').inputValue(), String(hours));
assert.equal(await page.locator('.lane').count(), 6);
assert.equal(await page.locator('.lane').count(), 7);
assert.equal(await page.locator('.lane').filter({ hasText: 'acme/widgets#2' }).locator('.time-run').count(), 2);
const complete = page.locator('.time-run').filter({ hasText: 'acme/widgets#3' });
const track = await complete.locator('.timeline').boundingBox();
Expand All @@ -98,21 +110,48 @@ test('Lanes scales every window, nests child Runs and fits 390 px without CSP vi
assert.deepEqual(errors, []);
});

test('time headings distinguish a live Pickup run and its Run and name kinds once at 390 px', { skip: process.platform !== 'linux' }, async t => {
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
t.after(() => context.close());
const page = await context.newPage();
for (const view of ['lanes', 'stages']) {
await page.goto(`${origin}/?page=${view}`);
const headings = view === 'lanes' ? page.locator('.time-run h3') : page.locator('thead .command');
for (const heading of ['Pickup run · acme/widgets#1', 'Run · acme/widgets#1', 'Spec run · acme/widgets#2', 'Run · acme/widgets#6', 'Security run on acme/widgets']) {
assert.equal(await headings.getByRole('link', { name: heading, exact: true }).count(), 1);
if (view === 'stages') {
assert.ok((await page.locator('.cell-run').allTextContents()).includes(`${heading}: `));
}
}
for (const heading of ['Pickup run · acme/widgets#1', 'Run · acme/widgets#1']) {
const link = headings.getByRole('link', { name: heading, exact: true });
assert.match(await link.locator('..').textContent(), /live/);
await link.click();
await page.locator('#peek-content h2').waitFor();
const details = await page.locator('#peek-content').textContent();
assert.ok(details.includes(heading.startsWith('Pickup') ? 'pickup-fixture' : 'run-1'));
await page.keyboard.press('Escape');
}
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
}
});

test('Stages pins live Runs, matches CLI order and opens the selected child Run at 390 px', { skip: process.platform !== 'linux' }, async t => {
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
t.after(() => context.close());
const page = await context.newPage();
await page.goto(`${origin}/?page=stages`);
const status = JSON.parse(execFileSync(binary, ['status', '--all', '--json'], { env: { ...process.env, HOME: home } }));
const runs = status.commands.flatMap(command => [command, ...(command.children ?? [])]);
const flatten = command => [command, ...(command.dispatched ?? []).flatMap(flatten), ...(command.children ?? []).flatMap(flatten)];
const runs = status.commands.flatMap(flatten);
runs.sort((a, b) => (a.state !== 'live') - (b.state !== 'live') || Date.parse(b.ended_at ?? b.started_at) - Date.parse(a.ended_at ?? a.started_at));
assert.deepEqual(await page.locator('thead .peek-link').allTextContents(), runs.map(run => `${run.repository}#${run.issue.number}`));
assert.equal(await page.locator('thead .command').first().getAttribute('data-command'), runs[0].record_path ?? `${join(home, '.thirdshift/logs/acme/widgets/commands/issue/1-fixture.command.json')}::run-1`);
assert.deepEqual(await page.locator('thead .peek-link').allTextContents(), runs.map(run => run.issue ? `${run.kind} · ${run.repository}#${run.issue.number}` : `${run.kind} on ${run.repository}`));
assert.equal(await page.locator('thead .command').first().getAttribute('data-command'), runs[0].record_path ?? `${join(home, '.thirdshift/logs/acme/widgets/commands/issue/1-fixture.command.json')}::pickup-fixture`);
assert.match(await page.locator('.stage-grid').first().textContent(), /Running ·/);
assert.match(await page.locator('.stage-grid').first().textContent(), /Usual range: 10–10 min/);
const historical = page.locator('.stage-grid').filter({ has: page.locator('thead .peek-link', { hasText: 'acme/widgets#9' }) });
const headers = await historical.locator('thead .peek-link').allTextContents();
const historicalCell = historical.locator('tbody tr').first().locator('td').nth(headers.indexOf('acme/widgets#9'));
const historicalCell = historical.locator('tbody tr').first().locator('td').nth(headers.indexOf('Run · acme/widgets#9'));
assert.match(await historicalCell.textContent(), /Unknown · no stage times retained/);
assert.doesNotMatch(await historicalCell.textContent(), /Not reached/);
const child = page.locator('thead .peek-link').filter({ hasText: 'acme/widgets#6' });
Expand Down
19 changes: 10 additions & 9 deletions src/logs/status/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anyhow::Result;
use chrono::{DateTime, Local};

use crate::config::UserConfig;
use crate::logs::command_record::StageName;
use crate::logs::command_record::{Issue, StageName};
use crate::logs::reading::{Command, Entry, Observation, Snapshot};

pub(crate) struct Views {
Expand Down Expand Up @@ -64,6 +64,13 @@ fn timestamp(value: &str) -> Option<i64> {
.map(|time| time.timestamp())
}

fn subject(kind: &str, repository: &str, issue: Option<&Issue>) -> String {
issue.map_or_else(
|| format!("{kind} on {repository}"),
|issue| format!("{kind} · {repository}#{}", issue.number),
)
}

fn run(command: &Command, now: DateTime<Local>, cutoff: i64) -> Run {
let record = &command.record;
let selector = Observation::Record(command).selector().unwrap_or_default();
Expand Down Expand Up @@ -127,10 +134,7 @@ fn run(command: &Command, now: DateTime<Local>, cutoff: i64) -> Run {
})
.collect();
Run {
subject: record.issue.as_ref().map_or_else(
|| format!("{} on {}", record.kind, record.repository),
|issue| format!("{}#{}", record.repository, issue.number),
),
subject: subject(&record.kind, &record.repository, record.issue.as_ref()),
title: record
.issue
.as_ref()
Expand Down Expand Up @@ -172,10 +176,7 @@ pub(crate) fn load(config: &UserConfig, hours: f64) -> Result<Views> {
Entry::History(command) => {
let selector = Observation::History(command).selector().unwrap_or_default();
runs.push(Run {
subject: command.issue.as_ref().map_or_else(
|| format!("{} on {}", command.kind, command.repository),
|issue| format!("{}#{}", command.repository, issue.number),
),
subject: subject(&command.kind, &command.repository, command.issue.as_ref()),
title: String::new(),
state: command
.outcome
Expand Down
46 changes: 46 additions & 0 deletions tests/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,52 @@ fn waterfall_nests_ticket_runs_and_matches_codex_items_once_in_their_scope() {
);
}

#[test]
fn time_pages_name_the_command_kind_without_repeating_it_for_commands_without_issues() {
let home = tempfile::tempdir().unwrap();
configure(home.path());
let mut pickup = record(home.path(), 7, Some("merged"), 0);
pickup["kind"] = json!("Pickup run");
pickup["name"] = json!("pickup-fixture");
pickup["stages"][0]["name"] = json!("dispatched Run");
let mut dispatched = pickup.clone();
dispatched["kind"] = json!("Run");
dispatched["name"] = json!("acme/widgets#7");
dispatched["stages"][0]["name"] = json!("implement");
pickup["dispatched"] = json!([dispatched]);
write_record(home.path(), 7, &pickup);
let mut security = record(home.path(), 8, Some("findings recorded"), 0);
security["kind"] = json!("Security run");
security["issue"] = Value::Null;
write_record(home.path(), 8, &security);
let now = chrono::Local::now();
let time = now.format("%Y-%m-%d %H:%M:%S");
let stamp = now.format("%Y%m%dT%H%M%S%z");
fs::write(
home.path().join(".thirdshift/logs/acme/widgets/activity.log"),
format!(
"{time} Spec run #9 started: commands/issue/9-{stamp}.log\n{time} Spec run #9 ended: merged\n"
),
)
.unwrap();
let console = Console::start(Path::new(env!("CARGO_BIN_EXE_thirdshift")), home.path());
for page in ["/lanes", "/stages"] {
let html = console.get(page).text().unwrap();
for heading in [
"Pickup run · acme/widgets#7",
"Run · acme/widgets#7",
"Security run on acme/widgets",
"Spec run · acme/widgets#9",
] {
assert!(html.contains(&format!(">{heading}</a>")), "{page}: {html}");
if page == "/stages" {
assert!(html.contains(&format!("class=\"cell-run\">{heading}: </span>")));
}
}
assert!(!html.contains("Security run · Security run"));
}
}

#[test]
fn time_pages_pair_with_all_status_stages_and_repository_usual_ranges() {
let home = tempfile::tempdir().unwrap();
Expand Down
Loading