fix(lanes): revive dead detect rules and stop alias-blind detection badges
Two defects that made the 16-node ship-feature pipeline look like stage detection was broken: - stage-detect matched each rule against a JOIN of the tool input's identifying fields. An Agent event really carries description + prompt + subagent_type, so `^Explore$` (intake) and `^Plan$` (plan) could never fire and both nodes shipped inert — the map jumped straight to `implementing`. Rules are now tested one field at a time, which also stops a rule matching across the seam between two unrelated fields. - `detectionLeadsDeclaration` (bin/ccam.js) and `detectionLeads` (LaneStripCard.tsx) located the declaration with `indexOf(lane.stage)`. Most of what the skill declares is an ALIAS (`tests`, `booting`, `ship`, `gate-blocked`), which is not a node id, so indexOf answered -1 and every detection — however far behind — was badged as leading. Both now use the node the server already resolved it onto (`state === "current"`). server 1346/1346, client 447/447.
This commit is contained in:
@@ -17,11 +17,22 @@ const LIVENESS_DOT: Record<Lane["liveness"], string> = {
|
||||
dead: "bg-status-danger",
|
||||
};
|
||||
|
||||
/** Whether the inferred stage sits ahead of the declared one in node order. */
|
||||
/**
|
||||
* Whether the inferred stage sits ahead of the declared one in node order.
|
||||
*
|
||||
* The declaration is located by the node the server resolved it onto (`state
|
||||
* === "current"`), not by `indexOf(lane.stage)`: a stage declared under an
|
||||
* ALIAS (`tests`, `booting`, `ship` — most of what the ship-feature skill
|
||||
* declares) is not a node id, so indexOf answers -1 and a detection sitting
|
||||
* well BEHIND the declaration would still be badged as the live one.
|
||||
*/
|
||||
function detectionLeads(lane: Lane): boolean {
|
||||
if (!lane.detected_stage) return false;
|
||||
const ids = lane.pipeline_nodes.map((n) => n.id);
|
||||
return ids.indexOf(lane.detected_stage) > ids.indexOf(lane.stage);
|
||||
const nodes = lane.pipeline_nodes;
|
||||
return (
|
||||
nodes.findIndex((n) => n.id === lane.detected_stage) >
|
||||
nodes.findIndex((n) => n.state === "current")
|
||||
);
|
||||
}
|
||||
|
||||
export default function LaneStripCard({
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @file Rendering tests for the Workspace lane tile, focused on the amber
|
||||
* "detected" badge: it may only appear when the inferred stage is genuinely
|
||||
* AHEAD of the declared one, including when the declaration used a node alias
|
||||
* (which is most of what the ship-feature skill declares).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import LaneStripCard from "../LaneStripCard";
|
||||
import type { Lane, LaneNode } from "../../../lib/types";
|
||||
|
||||
/** Four nodes of the ship-feature shape: `tests` is an ALIAS of `gates`. */
|
||||
function nodes(currentId: string): LaneNode[] {
|
||||
return (["intake", "implementing", "gates", "review"] as const).map((id) => ({
|
||||
id,
|
||||
label: id,
|
||||
icon: "",
|
||||
gate: false,
|
||||
state: id === currentId ? "current" : "pending",
|
||||
}));
|
||||
}
|
||||
|
||||
function lane(overrides: Partial<Lane>): Lane {
|
||||
return {
|
||||
id: 1,
|
||||
title: "lane",
|
||||
cwd: "/tmp/lane",
|
||||
branch: null,
|
||||
kind: "adopted",
|
||||
tool: "claude",
|
||||
source_repo: null,
|
||||
pipeline: "ship-feature",
|
||||
session_id: null,
|
||||
run_id: null,
|
||||
stage: "gates",
|
||||
stage_since: null,
|
||||
status: "running",
|
||||
gate_decision: null,
|
||||
ci_status: null,
|
||||
needs_action: null,
|
||||
links: {},
|
||||
stages: {},
|
||||
notes: null,
|
||||
pipeline_name: "Ship feature",
|
||||
pipeline_nodes: nodes("gates"),
|
||||
progress: 50,
|
||||
stage_seconds: null,
|
||||
last_event_seconds: null,
|
||||
liveness: "active",
|
||||
detected_stage: null,
|
||||
detected_signal: null,
|
||||
slot: null,
|
||||
ports: {},
|
||||
active_feature_id: null,
|
||||
...overrides,
|
||||
} as Lane;
|
||||
}
|
||||
|
||||
describe("LaneStripCard detected badge", () => {
|
||||
it("badges a detection that is ahead of the declaration", () => {
|
||||
render(
|
||||
<LaneStripCard
|
||||
lane={lane({ detected_stage: "review" })}
|
||||
selected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("lane-tile-auto-1")).toHaveTextContent("review");
|
||||
});
|
||||
|
||||
it("stays silent when the declaration was made under an alias and the detection is behind it", () => {
|
||||
// `tests` is an alias of the `gates` node — not a node id. Locating the
|
||||
// declaration by id would score it -1 and badge this stale `implementing`
|
||||
// detection as though the lane had moved backwards.
|
||||
render(
|
||||
<LaneStripCard
|
||||
lane={lane({ stage: "tests", detected_stage: "implementing" })}
|
||||
selected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId("lane-tile-auto-1")).toBeNull();
|
||||
});
|
||||
|
||||
it("still badges a detection ahead of an alias declaration", () => {
|
||||
render(
|
||||
<LaneStripCard
|
||||
lane={lane({ stage: "tests", detected_stage: "review" })}
|
||||
selected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("lane-tile-auto-1")).toHaveTextContent("review");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user