mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[1dae04a7] Video: release 0.25.0 (revision) (#536)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe get_latest_ci_conclusion defaults to the ladder's head rung, so wait_for_ci searched slave for a release commit that lives on master and timed out after 40 minutes with the run already green. The wait now passes the prod branch explicitly. Also fixes the react/no-unescaped-entities error that turned master's Panel CI red. * fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's click handler on the Tooltip root, which renders no DOM — the agents Spawn item and the KB Reindex-All / Delete-index confirms were dead. Tooltips now wrap the triggers. The video renderer accepts interior single dots in composition ids (release-0.25.0) with '..' still unrepresentable, and propose_video refuses an unrenderable id at authoring time. * fix(dispatch): restart-safe PM review turns A leaf task in awaiting_pm_review had no periodic pickup: the closure dispatcher bailed on childless tasks and skipped PR-bearing review tasks as already-promoted, assuming the submit-time PM session was still alive — an assumption every restart breaks. Proven live on the docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked its sibling dev task. Childless awaiting_pm_review tasks now flow to the PM's review turn, and the merge turn respawns its PM when none is active. * [1dae04a7] Revise release-0.25.0 composition to 40s scene-based pacing with four feature cards * [1dae04a7] docs(motion): update release-0.25.0 README section for 40s four-card revision --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
This commit is contained in:
co-authored by
Renn F
UX/UI Developer 1
UX/UI Documenter
parent
63212899ca
commit
797847e379
+11
-4
@@ -76,7 +76,14 @@ The smoke test (`release-recap.test.js`) asserts this schema, checks the counts,
|
||||
|
||||
`compositions/release-0.25.0/` is a panel-demo kit clip for the RoboCo v0.25.0 release. It builds on the `kit/` register instead of the text-card style, so it has no `theme.css` of its own.
|
||||
|
||||
The story is "governance gets a better UI": the CEO types "Governance gets a better UI" into the panel intake, then three shipped feature cards — Env ladder, Collision map, Metrics donut — enter a kanban column and flip from `in progress` to `completed`. A cursor clicks through the cards, a stats overlay flashes "1 release / 25 agents / 1 human", and a toast confirms "v0.25.0 shipped / I approved once. 25 agents did the rest." The clip closes on the "roboco.tech" outro.
|
||||
The current revision runs **40 seconds** total (up from an earlier 14s cut) so every feature card is fully visible before the next one enters. The story is still "governance gets a better UI": the CEO types "Governance gets a better UI" into the panel intake at 3.6s, then four shipped feature cards enter the kanban column one per scene and flip from `in progress` to `completed`:
|
||||
|
||||
1. **Env ladder** — enters at 5.0s, completes at 6.6s ("Dev to prod, one rung at a time.").
|
||||
2. **Collision map** — enters at 10.0s, completes at 11.6s ("See who touched what before you review.").
|
||||
3. **Metrics donut** — enters at 15.0s, completes at 16.6s ("90 days of real task flow.").
|
||||
4. **Notification bell** — enters at 20.0s, completes at 21.6s ("Real read/ack actions on every alert.").
|
||||
|
||||
Each card gets roughly five seconds of fully visible time before the next card enters. A cursor clicks the column at 22.0s, the stats overlay shows "1 release / 25 agents / 1 human" from 24.0s to 32.0s, the toast "v0.25.0 shipped / I approved once. 25 agents did the rest." runs from 30.0s to 38.0s, and the "roboco.tech" outro lands at 36.0s and holds through the end.
|
||||
|
||||
The composition reuses the same `pk-frame` chrome, `pk-column`/`pk-card`, `pk-pill`, `pk-cursor`, `pk-toast`, and `pk-outro` pieces from `kit/`, plus the typing reveal wired through `props.js`. Each feature card uses the `pk-pill--swap-out` / `pk-pill--swap-in` pattern from `panel-demo` and `release-recap` to replace the `in progress` pill with `completed` on the same beat.
|
||||
|
||||
@@ -101,14 +108,14 @@ pnpm test # release-0.25.0.test.js is picked up by vitest
|
||||
|
||||
### `captions.json`
|
||||
|
||||
Same schema as `release-recap`: one `captions.json` next to the HTML with self-verified X and TikTok captions:
|
||||
Same schema as `release-recap`: one `captions.json` next to the HTML with self-verified X and TikTok captions. The X caption was updated to include the Notification bell feature and now totals **216 characters**:
|
||||
|
||||
```json
|
||||
{
|
||||
"composition_id": "release-0.25.0",
|
||||
"occasion": "release: RoboCo v0.25.0",
|
||||
"platforms": {
|
||||
"x": { "caption": "...", "char_count": 170, "limit": 280, "within_limit": true },
|
||||
"x": { "caption": "...", "char_count": 216, "limit": 280, "within_limit": true },
|
||||
"tiktok": { "caption": "...", "char_count": 347, "limit": 2200, "within_limit": true }
|
||||
}
|
||||
}
|
||||
@@ -116,4 +123,4 @@ Same schema as `release-recap`: one `captions.json` next to the HTML with self-v
|
||||
|
||||
### Smoke-test invariants
|
||||
|
||||
`release-0.25.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with the HyperFrames params, the kit CSS/JS wiring is present, three feature cards each carry a progress-to-completed pill swap, the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, and no em dashes slip into on-screen copy or captions.
|
||||
`release-0.25.0.test.js` extends the panel-demo register checks: both `vertical.html` (1080×1920) and `square.html` (1080×1080) parse with `data-duration="40"` and the HyperFrames params, the kit CSS/JS wiring is present, **four** feature cards each carry a progress-to-completed pill swap and include the "Notification bell" text, the cursor and toast appear, the outro shows "roboco.tech", no external scripts are loaded, and no em dashes slip into on-screen copy or captions.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"occasion": "release: RoboCo v0.25.0",
|
||||
"platforms": {
|
||||
"x": {
|
||||
"caption": "RoboCo v0.25.0 is live. Env ladder from dev to prod. Collision map in every review. Metrics with a 90-day window. One human approved it; 25 agents shipped it. roboco.tech",
|
||||
"char_count": 170,
|
||||
"caption": "RoboCo v0.25.0 is live. Env ladder from dev to prod. Collision map in every review. Metrics with a 90-day window. Notification bell with real read/ack actions. One human approved it; 25 agents shipped it. roboco.tech",
|
||||
"char_count": 216,
|
||||
"limit": 280,
|
||||
"within_limit": true
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// HTML-structure smoke test for the RoboCo v0.25.0 release composition.
|
||||
// Extends the panel-demo register: panel chrome, typing reveal, feature cards
|
||||
// Extends the panel-demo register: panel chrome, typing reveal, four feature cards
|
||||
// with progress -> completed pill flips, cursor, stats overlay, toast,
|
||||
// outro, offline constraint, and the captions.json schema/limits.
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("release-0.25.0 composition", () => {
|
||||
|
||||
expect(html).toContain('data-width="1080"');
|
||||
expect(html).toContain(`data-height="${height}"`);
|
||||
expect(html).toMatch(/data-duration="14"/);
|
||||
expect(html).toMatch(/data-duration="40"/);
|
||||
expect(html).toMatch(/data-fps="30"/);
|
||||
|
||||
const propsIdx = html.indexOf('<script src="props.js"></script>');
|
||||
@@ -58,14 +58,15 @@ describe("release-0.25.0 composition", () => {
|
||||
const cardCount = (html.match(/pk-card clip/g) || []).length;
|
||||
const progressCount = (html.match(/pk-pill--progress/g) || []).length;
|
||||
const completedCount = (html.match(/pk-pill--completed/g) || []).length;
|
||||
expect(cardCount).toBe(3);
|
||||
expect(progressCount).toBe(3);
|
||||
expect(completedCount).toBe(3);
|
||||
expect(cardCount).toBe(4);
|
||||
expect(progressCount).toBe(4);
|
||||
expect(completedCount).toBe(4);
|
||||
|
||||
expect(html).toContain("v0.25.0");
|
||||
expect(html).toContain("Env ladder");
|
||||
expect(html).toContain("Collision map");
|
||||
expect(html).toContain("Metrics donut");
|
||||
expect(html).toContain("Notification bell");
|
||||
|
||||
expect(html).toContain("pk-cursor");
|
||||
expect(html).toContain("pk-cursor__ring");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="14" data-fps="30">
|
||||
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="40" data-fps="30">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>RoboCo v0.25.0 Release - square (1080×1080)</title>
|
||||
@@ -8,7 +8,7 @@
|
||||
<script src="../../kit/kit.js"></script>
|
||||
<style>
|
||||
/* Square cut of the v0.25.0 release clip. Tighter spacing, smaller hero,
|
||||
and the stats overlay covers the full frame for readability. */
|
||||
and the stats overlay covers the full frame for readability. Total 40s. */
|
||||
|
||||
.r25-hero {
|
||||
position: absolute;
|
||||
@@ -128,7 +128,7 @@
|
||||
z-index: 20;
|
||||
font-family: var(--pk-font-display);
|
||||
opacity: 0;
|
||||
animation: r25-stats-in 0.4s var(--pk-ease) 10.8s forwards, r25-stats-out 0.5s var(--pk-ease) 12.2s forwards;
|
||||
animation: r25-stats-in 0.4s var(--pk-ease) 24.0s forwards, r25-stats-out 0.5s var(--pk-ease) 32.0s forwards;
|
||||
}
|
||||
@keyframes r25-stats-in { to { opacity: 1; } }
|
||||
.r25-stats__line {
|
||||
@@ -161,9 +161,9 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1080" data-start="0" data-duration="14" data-fps="30">
|
||||
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1080" data-start="0" data-duration="40" data-fps="30">
|
||||
|
||||
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="13">
|
||||
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
|
||||
<div class="r25-hero__markwrap">
|
||||
<img class="r25-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
|
||||
<div class="r25-hero__ring"></div>
|
||||
@@ -172,7 +172,7 @@
|
||||
<div class="r25-hero__tag">governance gets a better UI</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-frame clip" data-start="2.6" data-duration="11.4" data-track-index="1">
|
||||
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
|
||||
<aside class="pk-frame__sidebar">
|
||||
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
|
||||
<div class="r25-nav">Overview</div>
|
||||
@@ -188,7 +188,7 @@
|
||||
<header class="pk-frame__topbar">
|
||||
<div class="pk-frame__search">Search tasks, agents...</div>
|
||||
<div class="r25-user">
|
||||
<div class="pk-frame__status clip" data-start="2.6" data-duration="11.4" data-track-index="0">
|
||||
<div class="pk-frame__status clip" data-start="2.6" data-duration="37.4" data-track-index="0">
|
||||
<span class="pk-frame__statusdot"></span>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@
|
||||
</header>
|
||||
|
||||
<div class="pk-frame__content">
|
||||
<div class="pk-intake clip" data-start="3.6" data-duration="10.4" data-track-index="2" style="animation-delay: 3.6s;">
|
||||
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
|
||||
<div class="pk-intake__label">New task</div>
|
||||
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
|
||||
</div>
|
||||
@@ -205,55 +205,67 @@
|
||||
<div class="pk-column">
|
||||
<div class="pk-column__header">Shipped in v0.25.0</div>
|
||||
|
||||
<div class="pk-card clip" data-start="5.2" data-duration="8.8" data-track-index="3" style="animation-delay: 5.2s;">
|
||||
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
|
||||
<div class="pk-card__title">Env ladder</div>
|
||||
<div class="r25-sub">Dev to prod, one rung at a time.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">project settings</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.2" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.2s; --pk-pill-exit-delay: 6.6s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.8" data-duration="7.2" data-track-index="5" style="--pk-pill-enter-delay: 6.8s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="7" data-duration="7" data-track-index="6" style="animation-delay: 7s;">
|
||||
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
|
||||
<div class="pk-card__title">Collision map</div>
|
||||
<div class="r25-sub">See who touched what before you review.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">task detail</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="7" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 7s; --pk-pill-exit-delay: 8.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="8.6" data-duration="5.4" data-track-index="8" style="--pk-pill-enter-delay: 8.6s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="8.8" data-duration="5.2" data-track-index="9" style="animation-delay: 8.8s;">
|
||||
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
|
||||
<div class="pk-card__title">Metrics donut</div>
|
||||
<div class="r25-sub">90 days of real task flow.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">performance tab</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.8" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 8.8s; --pk-pill-exit-delay: 10.2s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10.4" data-duration="3.6" data-track-index="11" style="--pk-pill-enter-delay: 10.4s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
|
||||
<div class="pk-card__title">Notification bell</div>
|
||||
<div class="r25-sub">Real read/ack actions on every alert.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">notifications</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-cursor clip" data-start="9.4" data-duration="4.6" data-track-index="12" style="--pk-cursor-delay: 9.4s; --pk-cursor-x0: 700px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1110px;">
|
||||
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 700px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 700px;">
|
||||
<div class="pk-cursor__glyph"></div>
|
||||
<div class="pk-cursor__ring" style="--pk-click-delay: 10.4s;"></div>
|
||||
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="r25-stats clip" data-start="10.8" data-duration="3.2" data-track-index="14">
|
||||
<div class="r25-stats__line" style="animation-delay: 10.8s;">1 release</div>
|
||||
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 11.1s;">25 agents</div>
|
||||
<div class="r25-stats__line" style="animation-delay: 11.4s;">1 human</div>
|
||||
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
|
||||
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
|
||||
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
|
||||
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-toast clip" data-start="11.8" data-duration="2.2" data-track-index="15" style="animation-delay: 11.8s;">
|
||||
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
|
||||
<div class="pk-toast__icon"></div>
|
||||
<div class="pk-toast__body">
|
||||
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
|
||||
@@ -261,7 +273,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-outro clip" data-start="12.6" data-duration="1.4" data-track-index="16" style="animation-delay: 12.6s;">
|
||||
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
|
||||
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
|
||||
<span>roboco.tech</span>
|
||||
</div>
|
||||
@@ -273,7 +285,7 @@
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["release-0.25.0"] = {
|
||||
id: "release-0.25.0",
|
||||
duration: 14,
|
||||
duration: 40,
|
||||
fps: 30,
|
||||
animations: []
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="14" data-fps="30">
|
||||
<html lang="en" data-composition-id="release-0.25.0" data-composition-duration="40" data-fps="30">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>RoboCo v0.25.0 Release - vertical (1080×1920)</title>
|
||||
@@ -8,8 +8,8 @@
|
||||
<script src="../../kit/kit.js"></script>
|
||||
<style>
|
||||
/* v0.25.0 release clip - panel-demo register. Identity cold open,
|
||||
panel assembles, three feature cards flip to completed, cursor click,
|
||||
receipt stats, toast, lockup. */
|
||||
panel assembles, four feature cards flip to completed one per scene,
|
||||
cursor click, receipt stats, toast, lockup. Total 40s. */
|
||||
|
||||
/* ---- Identity cold open -------------------------------------------- */
|
||||
.r25-hero {
|
||||
@@ -149,7 +149,7 @@
|
||||
animation: pk-enter-rise-scale 0.55s var(--pk-ease) forwards;
|
||||
}
|
||||
.r25-stats__line--accent { color: var(--pk-accent); }
|
||||
.r25-stats { animation: r25-stats-out 0.5s var(--pk-ease) 11.6s forwards; }
|
||||
.r25-stats { animation: r25-stats-out 0.5s var(--pk-ease) 32.0s forwards; }
|
||||
@keyframes r25-stats-out { to { opacity: 0; } }
|
||||
|
||||
.pk-toast { --pk-toast-bottom: 140px; }
|
||||
@@ -177,10 +177,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1920" data-start="0" data-duration="14" data-fps="30">
|
||||
<div id="stage" data-composition-id="release-0.25.0" data-width="1080" data-height="1920" data-start="0" data-duration="40" data-fps="30">
|
||||
|
||||
<!-- Beat 1: identity cold open. -->
|
||||
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="13">
|
||||
<div class="r25-hero clip" data-start="0" data-duration="3.2" data-track-index="16">
|
||||
<div class="r25-hero__markwrap">
|
||||
<img class="r25-hero__mark" src="../../public/roboco-logo-darkmode.png" alt="RoboCo" />
|
||||
<div class="r25-hero__ring"></div>
|
||||
@@ -190,7 +190,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Beat 2: the panel assembles. -->
|
||||
<div class="pk-frame clip" data-start="2.6" data-duration="11.4" data-track-index="1">
|
||||
<div class="pk-frame clip" data-start="2.6" data-duration="37.4" data-track-index="1">
|
||||
<aside class="pk-frame__sidebar">
|
||||
<div class="r25-side__logo"><img src="../../public/roboco-logo.png" alt="" /></div>
|
||||
<div class="r25-nav">Overview</div>
|
||||
@@ -206,7 +206,7 @@
|
||||
<header class="pk-frame__topbar">
|
||||
<div class="pk-frame__search">Search tasks, agents...</div>
|
||||
<div class="r25-user">
|
||||
<div class="pk-frame__status clip" data-start="2.6" data-duration="11.4" data-track-index="0">
|
||||
<div class="pk-frame__status clip" data-start="2.6" data-duration="37.4" data-track-index="0">
|
||||
<span class="pk-frame__statusdot"></span>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
@@ -216,67 +216,79 @@
|
||||
|
||||
<div class="pk-frame__content">
|
||||
<!-- Beat 3: the hook types into intake. -->
|
||||
<div class="pk-intake clip" data-start="3.6" data-duration="10.4" data-track-index="2" style="animation-delay: 3.6s;">
|
||||
<div class="pk-intake clip" data-start="3.6" data-duration="36.4" data-track-index="2" style="animation-delay: 3.6s;">
|
||||
<div class="pk-intake__label">New task</div>
|
||||
<div class="pk-intake__field"><span id="typeIntro">Governance gets a better UI</span><span class="pk-caret"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Beat 4: three feature cards stack and complete. -->
|
||||
<!-- Beat 4: four feature cards, one per ~5s scene, each fully visible before the next enters. -->
|
||||
<div class="pk-column">
|
||||
<div class="pk-column__header">Shipped in v0.25.0</div>
|
||||
|
||||
<div class="pk-card clip" data-start="5.2" data-duration="8.8" data-track-index="3" style="animation-delay: 5.2s;">
|
||||
<div class="pk-card clip" data-start="5.0" data-duration="35" data-track-index="3" style="animation-delay: 5.0s;">
|
||||
<div class="pk-card__title">Env ladder</div>
|
||||
<div class="r25-sub">Dev to prod, one rung at a time.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">project settings</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.2" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.2s; --pk-pill-exit-delay: 6.6s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.8" data-duration="7.2" data-track-index="5" style="--pk-pill-enter-delay: 6.8s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="5.0" data-duration="1.6" data-track-index="4" style="--pk-pill-enter-delay: 5.0s; --pk-pill-exit-delay: 6.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="6.6" data-duration="33.4" data-track-index="5" style="--pk-pill-enter-delay: 6.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="7" data-duration="7" data-track-index="6" style="animation-delay: 7s;">
|
||||
<div class="pk-card clip" data-start="10.0" data-duration="30" data-track-index="6" style="animation-delay: 10.0s;">
|
||||
<div class="pk-card__title">Collision map</div>
|
||||
<div class="r25-sub">See who touched what before you review.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">task detail</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="7" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 7s; --pk-pill-exit-delay: 8.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="8.6" data-duration="5.4" data-track-index="8" style="--pk-pill-enter-delay: 8.6s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="10.0" data-duration="1.6" data-track-index="7" style="--pk-pill-enter-delay: 10.0s; --pk-pill-exit-delay: 11.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="11.6" data-duration="28.4" data-track-index="8" style="--pk-pill-enter-delay: 11.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="8.8" data-duration="5.2" data-track-index="9" style="animation-delay: 8.8s;">
|
||||
<div class="pk-card clip" data-start="15.0" data-duration="25" data-track-index="9" style="animation-delay: 15.0s;">
|
||||
<div class="pk-card__title">Metrics donut</div>
|
||||
<div class="r25-sub">90 days of real task flow.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">performance tab</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="8.8" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 8.8s; --pk-pill-exit-delay: 10.2s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="10.4" data-duration="3.6" data-track-index="11" style="--pk-pill-enter-delay: 10.4s;">completed</span>
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="15.0" data-duration="1.6" data-track-index="10" style="--pk-pill-enter-delay: 15.0s; --pk-pill-exit-delay: 16.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="16.6" data-duration="23.4" data-track-index="11" style="--pk-pill-enter-delay: 16.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-card clip" data-start="20.0" data-duration="20" data-track-index="12" style="animation-delay: 20.0s;">
|
||||
<div class="pk-card__title">Notification bell</div>
|
||||
<div class="r25-sub">Real read/ack actions on every alert.</div>
|
||||
<div class="pk-card__meta">
|
||||
<span class="pk-chip pk-chip--gray">notifications</span>
|
||||
</div>
|
||||
<div class="pk-card__status">
|
||||
<span class="pk-pill pk-pill--progress pk-pill--swap-out clip" data-start="20.0" data-duration="1.6" data-track-index="13" style="--pk-pill-enter-delay: 20.0s; --pk-pill-exit-delay: 21.4s;">in progress</span>
|
||||
<span class="pk-pill pk-pill--completed pk-pill--swap-in clip" data-start="21.6" data-duration="18.4" data-track-index="14" style="--pk-pill-enter-delay: 21.6s;">completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cursor glide to the newest card. -->
|
||||
<div class="pk-cursor clip" data-start="9.4" data-duration="4.6" data-track-index="12" style="--pk-cursor-delay: 9.4s; --pk-cursor-x0: 720px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1110px;">
|
||||
<div class="pk-cursor clip" data-start="22.0" data-duration="6" data-track-index="15" style="--pk-cursor-delay: 22.0s; --pk-cursor-x0: 720px; --pk-cursor-y0: 300px; --pk-cursor-x1: 200px; --pk-cursor-y1: 1090px;">
|
||||
<div class="pk-cursor__glyph"></div>
|
||||
<div class="pk-cursor__ring" style="--pk-click-delay: 10.4s;"></div>
|
||||
<div class="pk-cursor__ring" style="--pk-click-delay: 23.4s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Beat 5: receipt stats. -->
|
||||
<div class="r25-stats clip" data-start="10.8" data-duration="3.2" data-track-index="14">
|
||||
<div class="r25-stats__line" style="animation-delay: 10.8s;">1 release</div>
|
||||
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 11.1s;">25 agents</div>
|
||||
<div class="r25-stats__line" style="animation-delay: 11.4s;">1 human</div>
|
||||
<div class="r25-stats clip" data-start="24.0" data-duration="8" data-track-index="17">
|
||||
<div class="r25-stats__line" style="animation-delay: 24.0s;">1 release</div>
|
||||
<div class="r25-stats__line r25-stats__line--accent" style="animation-delay: 24.3s;">25 agents</div>
|
||||
<div class="r25-stats__line" style="animation-delay: 24.6s;">1 human</div>
|
||||
</div>
|
||||
|
||||
<!-- Beat 6: confirmation toast, then lockup. -->
|
||||
<div class="pk-toast clip" data-start="11.8" data-duration="2.2" data-track-index="15" style="animation-delay: 11.8s;">
|
||||
<div class="pk-toast clip" data-start="30.0" data-duration="8" data-track-index="18" style="animation-delay: 30.0s;">
|
||||
<div class="pk-toast__icon"></div>
|
||||
<div class="pk-toast__body">
|
||||
<div class="pk-toast__title" id="toastTitle">v0.25.0 shipped</div>
|
||||
@@ -284,7 +296,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pk-outro clip" data-start="12.6" data-duration="1.4" data-track-index="16" style="animation-delay: 12.6s;">
|
||||
<div class="pk-outro clip" data-start="36.0" data-duration="4" data-track-index="19" style="animation-delay: 36.0s;">
|
||||
<span class="r25-outro__logo"><img src="../../public/roboco-logo.png" alt="" /></span>
|
||||
<span>roboco.tech</span>
|
||||
</div>
|
||||
@@ -296,7 +308,7 @@
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["release-0.25.0"] = {
|
||||
id: "release-0.25.0",
|
||||
duration: 14,
|
||||
duration: 40,
|
||||
fps: 30,
|
||||
animations: []
|
||||
};
|
||||
|
||||
@@ -70,18 +70,28 @@ export function SpawnAgentDialog({
|
||||
setInitialPrompt("");
|
||||
};
|
||||
|
||||
// The tooltip must wrap the DialogTrigger, never sit inside it: with
|
||||
// HelpTip as DialogTrigger's asChild child, the dialog's click handler is
|
||||
// cloned onto the Tooltip root (not a DOM element) and silently dropped.
|
||||
const defaultTrigger = (
|
||||
<HelpTip label="Start this agent's container, optionally pre-claiming a task" side="left">
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Spawn
|
||||
</DropdownMenuItem>
|
||||
</HelpTip>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Spawn
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger || defaultTrigger}</DialogTrigger>
|
||||
{trigger ? (
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
) : (
|
||||
<HelpTip
|
||||
label="Start this agent's container, optionally pre-claiming a task"
|
||||
side="left"
|
||||
>
|
||||
<DialogTrigger asChild>{defaultTrigger}</DialogTrigger>
|
||||
</HelpTip>
|
||||
)}
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Spawn {agentName}</DialogTitle>
|
||||
|
||||
@@ -555,14 +555,14 @@ function KnowledgeBaseBrowserContent() {
|
||||
<CardTitle className="text-sm flex items-center justify-between">
|
||||
<span>Summary</span>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
|
||||
<HelpTip label="Rebuilds the Documentation index from the repo's docs/ tree — the other categories (journals, conversations, etc.) are populated live by agent activity, not by this button">
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="destructive">
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Reindex All
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</AlertDialogTrigger>
|
||||
</AlertDialogTrigger>
|
||||
</HelpTip>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reindex All Data?</AlertDialogTitle>
|
||||
@@ -679,8 +679,8 @@ function KnowledgeBaseBrowserContent() {
|
||||
</Button>
|
||||
</HelpTip>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<HelpTip label="Delete this index and all its documents">
|
||||
<HelpTip label="Delete this index and all its documents">
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -689,8 +689,8 @@ function KnowledgeBaseBrowserContent() {
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</HelpTip>
|
||||
</AlertDialogTrigger>
|
||||
</AlertDialogTrigger>
|
||||
</HelpTip>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
|
||||
@@ -125,7 +125,7 @@ export function Header() {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Signed in as the CEO — the panel's single human operator
|
||||
Signed in as the CEO — the panel's single human operator
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -12090,11 +12090,15 @@ Start now: evidence(task_id="{task_id}")
|
||||
|
||||
@staticmethod
|
||||
def _already_promoted_for_closure(task: dict[str, Any]) -> bool:
|
||||
"""Skip closure respawn when PR+status show task has moved up."""
|
||||
"""Skip closure respawn when PR+status show task has moved past the PM.
|
||||
|
||||
``awaiting_pm_review`` is deliberately NOT in the skip set: it is the
|
||||
PM's own merge/review turn, and the session that submitted may be gone
|
||||
(restart, idle-reap) — the ``_is_agent_active`` check below already
|
||||
prevents double-spawning while a PM is genuinely alive."""
|
||||
return bool(
|
||||
task.get("pr_number")
|
||||
and task.get("status")
|
||||
in ("awaiting_pm_review", "awaiting_ceo_approval", "completed")
|
||||
and task.get("status") in ("awaiting_ceo_approval", "completed")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -12314,7 +12318,15 @@ Start now: evidence(task_id="{task_id}")
|
||||
return
|
||||
|
||||
descendants = await self._fetch_all_descendants(client, task_id)
|
||||
if not descendants or not self._all_descendants_terminal(descendants):
|
||||
# A childless task in awaiting_pm_review IS the PM's review turn —
|
||||
# its dev→qa→doc stages ran on the task itself. Without this, a leaf
|
||||
# review stranded by a restart (the submit-time PM session gone) had
|
||||
# no periodic pickup at all — proven live on the docs-sync leaf
|
||||
# after the 0.25.0 redeploy.
|
||||
is_leaf_review = not descendants and task.get("status") == "awaiting_pm_review"
|
||||
if not is_leaf_review and (
|
||||
not descendants or not self._all_descendants_terminal(descendants)
|
||||
):
|
||||
return
|
||||
if self._already_promoted_for_closure(task):
|
||||
return
|
||||
|
||||
@@ -342,6 +342,8 @@ _CURATE_VAULT_ROLES: frozenset[str] = frozenset({"auditor"})
|
||||
# reuses MAX_TWEET_CHARS). No role frozenset here, unlike the sets above:
|
||||
# propose_video is gated on the caller's TEAM at runtime (_caller_team), not
|
||||
# role — Role.DEVELOPER doesn't distinguish a ux-dev from a be-dev/fe-dev.
|
||||
# Kept in lockstep with video-renderer/server.js COMPOSITION_ID_RE.
|
||||
_COMPOSITION_ID_RE = re.compile(r"^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$")
|
||||
_VIDEO_PLATFORMS: frozenset[str] = frozenset({"x", "tiktok"})
|
||||
_MAX_TIKTOK_CAPTION_CHARS = 2200
|
||||
|
||||
@@ -1532,6 +1534,20 @@ class ContentActions:
|
||||
statement count under the xenon/PLR0911 budget)."""
|
||||
if rej := cls._reject_soup(composition_id, field="composition_id", min_chars=2):
|
||||
return rej
|
||||
# Mirror the video-renderer sidecar's charset rule so an unrenderable
|
||||
# id is refused at authoring time, not at render time days later.
|
||||
if not _COMPOSITION_ID_RE.fullmatch(composition_id.strip()):
|
||||
return Envelope.invalid_state(
|
||||
message=(
|
||||
f"composition_id {composition_id!r} is not renderable — "
|
||||
"letters, digits, '_' or '-' with optional interior dots"
|
||||
),
|
||||
remediate=(
|
||||
"rename the composition dir to match (e.g. "
|
||||
"'release-0-25-0' or 'release-0.25.0') and call "
|
||||
"propose_video again with that id"
|
||||
),
|
||||
)
|
||||
if rej := cls._reject_caption(
|
||||
x_caption, field="x_caption", max_chars=MAX_TWEET_CHARS
|
||||
):
|
||||
|
||||
@@ -2202,6 +2202,7 @@ class GitService(BaseService):
|
||||
*,
|
||||
workflow: str | None = None,
|
||||
head_sha: str | None = None,
|
||||
branch: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Latest completed CI (GitHub Actions) run on a project's default branch.
|
||||
|
||||
@@ -2229,7 +2230,10 @@ class GitService(BaseService):
|
||||
git_token = await self._token_for_project(project_slug)
|
||||
if not git_token:
|
||||
return None
|
||||
branch = head_branch(project)
|
||||
# Default to the head rung (where dev work and the release gate look);
|
||||
# the release-commit CI wait overrides with the prod rung, where the
|
||||
# pushed release commit actually lives.
|
||||
branch = branch or head_branch(project)
|
||||
query = _CiRunQuery(
|
||||
project_slug=project_slug,
|
||||
owner_repo=(owner, repo),
|
||||
|
||||
@@ -449,7 +449,10 @@ class _GitReleaseOps:
|
||||
git = get_git_service(self._session)
|
||||
for _ in range(_CI_MAX_POLLS):
|
||||
ci = await git.get_latest_ci_conclusion(
|
||||
self._slug, workflow=self._ci_workflow, head_sha=commit_sha
|
||||
self._slug,
|
||||
workflow=self._ci_workflow,
|
||||
head_sha=commit_sha,
|
||||
branch=self._default_branch,
|
||||
)
|
||||
if ci and ci.get("head_sha") == commit_sha:
|
||||
conclusion = (ci.get("conclusion") or "").lower()
|
||||
|
||||
@@ -286,3 +286,14 @@ async def test_propose_video_defaults_input_props_to_empty_dict(
|
||||
draft = markers.get_video_draft(authoring)
|
||||
assert draft is not None
|
||||
assert draft["input_props"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_propose_video_rejects_unrenderable_composition_id() -> None:
|
||||
"""An id the renderer's charset rule would 400 is refused at authoring
|
||||
time, not at render time days later."""
|
||||
env = await _actions("developer", "ux_ui").propose_video(
|
||||
agent_id=uuid4(), **_valid_kwargs(composition_id="release 0.25.0!")
|
||||
)
|
||||
assert env.error == "invalid_state"
|
||||
assert "not renderable" in (env.message or "")
|
||||
|
||||
@@ -161,3 +161,38 @@ async def test_auto_recover_blocked_swallows_errors() -> None:
|
||||
|
||||
# Must not raise.
|
||||
await orch._auto_recover_blocked_parent(client, "p")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_childless_awaiting_pm_review_reaches_closure() -> None:
|
||||
"""A leaf task in awaiting_pm_review (dev->qa->doc ran on the task itself)
|
||||
is the PM's review turn — it must flow past the descendants gate, or a
|
||||
restart-stranded review has no periodic pickup at all."""
|
||||
orch = _ready_orch()
|
||||
orch._fetch_all_descendants = AsyncMock(return_value=[])
|
||||
orch._closure_handled_without_pm = AsyncMock(return_value=(True, None))
|
||||
task = {"id": "t1", "status": "awaiting_pm_review", "team": "frontend"}
|
||||
await orch._maybe_spawn_pm_closure(MagicMock(), task)
|
||||
orch._closure_handled_without_pm.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_childless_in_progress_still_bails() -> None:
|
||||
"""A childless claimed/in_progress task is a dev's work, not PM closure
|
||||
material — the descendants gate must still bail."""
|
||||
orch = _ready_orch()
|
||||
orch._fetch_all_descendants = AsyncMock(return_value=[])
|
||||
orch._closure_handled_without_pm = AsyncMock(return_value=(True, None))
|
||||
task = {"id": "t1", "status": "in_progress", "team": "frontend"}
|
||||
await orch._maybe_spawn_pm_closure(MagicMock(), task)
|
||||
orch._closure_handled_without_pm.assert_not_awaited()
|
||||
|
||||
|
||||
def test_promoted_skip_excludes_the_pm_merge_turn() -> None:
|
||||
"""awaiting_pm_review IS the PM's turn — a PR-bearing task there must not
|
||||
be skipped as already-promoted (the submit-time PM session may be gone)."""
|
||||
promoted = AgentOrchestrator._already_promoted_for_closure
|
||||
assert not promoted({"pr_number": 5, "status": "awaiting_pm_review"})
|
||||
assert promoted({"pr_number": 5, "status": "awaiting_ceo_approval"})
|
||||
assert promoted({"pr_number": 5, "status": "completed"})
|
||||
assert not promoted({"pr_number": None, "status": "completed"})
|
||||
|
||||
@@ -587,3 +587,34 @@ def test_insert_changelog_entry_without_unreleased_is_unchanged_behavior() -> No
|
||||
result = re._insert_changelog_entry(existing, entry)
|
||||
assert result.index("## [0.25.0]") < result.index("## [0.24.0]")
|
||||
assert "- old" in result and "- new" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_ci_polls_the_prod_branch(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""The release commit lives on the prod rung — the CI wait must query that
|
||||
branch, not the ladder head where get_latest_ci_conclusion defaults."""
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
async def _fake_get_ci(_slug: str, **kwargs: object) -> dict[str, object]:
|
||||
seen.update(kwargs)
|
||||
return {"head_sha": "cafebabe", "conclusion": "success"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"roboco.services.git.get_git_service",
|
||||
lambda _session: SimpleNamespace(get_latest_ci_conclusion=_fake_get_ci),
|
||||
)
|
||||
ctx = _ReleaseContext(
|
||||
slug="roboco-api",
|
||||
prod_branch="master",
|
||||
root=tmp_path,
|
||||
git_url="x",
|
||||
git_prefix=[],
|
||||
ci_workflow="ci.yml",
|
||||
env_chain=["slave"],
|
||||
)
|
||||
ops = _GitReleaseOps(session=MagicMock(), ctx=ctx)
|
||||
ok = await ops.wait_for_ci("cafebabe")
|
||||
assert ok is True
|
||||
assert seen.get("branch") == "master"
|
||||
|
||||
@@ -24,7 +24,10 @@ const PORT = Number(process.env.PORT ?? 3001);
|
||||
// safe path segment so a ".." / "/" / absolute-path value can't escape the
|
||||
// composition dir (path traversal). The orchestrator only ever sends a real
|
||||
// composition id, but this is the trust boundary — validate here.
|
||||
const COMPOSITION_ID_RE = /^[A-Za-z0-9_-]+$/;
|
||||
// Letters/digits/_/- plus interior single dots (e.g. release-0.25.0).
|
||||
// No leading dot and no adjacent dots, so '.'/'..' path segments and
|
||||
// hidden-file names remain unrepresentable — this stays the trust boundary.
|
||||
const COMPOSITION_ID_RE = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
|
||||
|
||||
// motion/ source (no node_modules, no build output) is a few hundred KB in
|
||||
// practice; this cap is generous headroom, not a tuned budget.
|
||||
@@ -74,7 +77,7 @@ app.post("/render", renderLimiter, upload.single("source"), async (req, res) =>
|
||||
) {
|
||||
res.status(400).json({
|
||||
error:
|
||||
"'composition_id' must be a non-empty string of letters, digits, '_' or '-'",
|
||||
"'composition_id' must be letters, digits, '_' or '-', with optional interior dots",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user