fix(lanes): expire stale stage detections, cover raw test runs in ship-feature gates
withDetected() rendered lane.detected_stage forever once written — the TTL in recordDetection only stops future writes from advancing past a stale floor, it never clears the row, so a lane gone quiet kept painting amber nodes from activity that was no longer current. Also add the shared test-runner regex to the ship-feature pipeline's `gates` node: Stage 1b's independent-test subagent runs the repo's test command directly, not through `ccam`, and had no detection fallback if `ccam stage tests` was skipped.
This commit is contained in:
@@ -901,6 +901,26 @@ describe("stage detection", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a stale detection stops rendering as `detected` in pipeline_nodes, even though the DB row still holds it", () => {
|
||||||
|
const { db } = require("../db");
|
||||||
|
const l = lanes.createLane({ cwd: "/tmp/wt-detect-stale-render" });
|
||||||
|
lanes.recordDetection(l.id, { nodeId: "tests" });
|
||||||
|
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
||||||
|
db.prepare("UPDATE lanes SET detected_at = ? WHERE id = ?").run(oldTime, l.id);
|
||||||
|
|
||||||
|
const lane = lanes.getLane(l.id);
|
||||||
|
// The TTL only gates future writes; the stale row is still there until a
|
||||||
|
// fresh detection or a real stage transition overwrites it.
|
||||||
|
assert.equal(lane.detected_stage, "tests");
|
||||||
|
|
||||||
|
const nodes = lanes.lanePayload(lane).pipeline_nodes;
|
||||||
|
assert.ok(
|
||||||
|
!nodes.some((n) => n.detected),
|
||||||
|
"a detection older than the TTL must not paint any node amber — it is no longer current information"
|
||||||
|
);
|
||||||
|
lanes.deleteLane(l.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("the forward-only hold is short enough for one work session", () => {
|
describe("the forward-only hold is short enough for one work session", () => {
|
||||||
|
|||||||
@@ -521,6 +521,10 @@ test.describe("ship-feature pipeline template", () => {
|
|||||||
["implementing", "Write", { file_path: "server/lib/app.js" }],
|
["implementing", "Write", { file_path: "server/lib/app.js" }],
|
||||||
["gates", "Bash", { command: "ccam lanes hook ci-gate" }],
|
["gates", "Bash", { command: "ccam lanes hook ci-gate" }],
|
||||||
["gates", "Bash", { command: "ccam lanes sync-base --check feat/foo" }],
|
["gates", "Bash", { command: "ccam lanes sync-base --check feat/foo" }],
|
||||||
|
// Stage 1b's independent-test subagent runs the repo's raw test command
|
||||||
|
// directly, not through `ccam` — without this rule that pass has no
|
||||||
|
// detection fallback if the agent skips `ccam stage tests`.
|
||||||
|
["gates", "Bash", { command: "npm run test:server" }],
|
||||||
["e2e-feature", "Bash", { command: "ccam lanes up --qc" }],
|
["e2e-feature", "Bash", { command: "ccam lanes up --qc" }],
|
||||||
["e2e-feature", "Bash", { command: "ccam lanes hook e2e" }],
|
["e2e-feature", "Bash", { command: "ccam lanes hook e2e" }],
|
||||||
["review", "Skill", { skill: "code-review" }],
|
["review", "Skill", { skill: "code-review" }],
|
||||||
|
|||||||
@@ -96,6 +96,10 @@
|
|||||||
{
|
{
|
||||||
"tool": "Bash",
|
"tool": "Bash",
|
||||||
"match": "\\bccam\\b[^;&|]*(\\bhook ci-gate\\b|\\bsync-base --check\\b)"
|
"match": "\\bccam\\b[^;&|]*(\\bhook ci-gate\\b|\\bsync-base --check\\b)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "Bash",
|
||||||
|
"match": "\\b(npm (run )?test|pytest|vitest|jest|go test|cargo test|node\\s+--test|rspec|phpunit|dotnet test|mvn test|gradle test)\\b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -466,8 +466,15 @@ function classifyLiveness({ status, stage, ageSec }, deadSec = DEAD_SEC) {
|
|||||||
* would otherwise render as an inference instead of the blue `current` ring.
|
* would otherwise render as an inference instead of the blue `current` ring.
|
||||||
* Past nodes go through `stageRecords`, which resolves those alias keys — a
|
* Past nodes go through `stageRecords`, which resolves those alias keys — a
|
||||||
* node the agent DECLARED must never be painted as merely detected.
|
* node the agent DECLARED must never be painted as merely detected.
|
||||||
|
*
|
||||||
|
* A detection older than the TTL is treated the same as no detection: the TTL
|
||||||
|
* in recordDetection only stops it from holding the forward-only floor for
|
||||||
|
* FUTURE writes, it never clears the row, so without this check a lane that
|
||||||
|
* went quiet would keep painting amber nodes from activity that is no longer
|
||||||
|
* current — the exact stale-progress illusion detection exists to avoid.
|
||||||
*/
|
*/
|
||||||
function withDetected(states, pipeline, lane) {
|
function withDetected(states, pipeline, lane) {
|
||||||
|
if (detectionIsStale(lane.detected_at)) return states.map((n) => ({ ...n, detected: false }));
|
||||||
const detectedIdx = phaseIdx(pipeline, lane.detected_stage);
|
const detectedIdx = phaseIdx(pipeline, lane.detected_stage);
|
||||||
if (detectedIdx === -1) return states.map((n) => ({ ...n, detected: false }));
|
if (detectedIdx === -1) return states.map((n) => ({ ...n, detected: false }));
|
||||||
const records = stageRecords(pipeline, lane.stages);
|
const records = stageRecords(pipeline, lane.stages);
|
||||||
|
|||||||
Reference in New Issue
Block a user