mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e086fd343b | ||
|
|
e15cebdf67 |
No files matched your search
@@ -161,6 +161,15 @@ or `pending`, `draft`. A word that matches none of these counts as `active`, so
|
||||
slice marked `complete` counts as `active`. A slice with no `status:` counts as
|
||||
`draft`.
|
||||
|
||||
`rig view show execution` shows the same word and bucket beside each slice's
|
||||
ladder as `recorded_status` (`word: null` when there's no `status:`). It's the
|
||||
team's record, not verified, so it never turns a rung true or makes a slice next-up.
|
||||
The ladder's rungs read Part B records: plan lock, `candidate:<sha>` rows, review
|
||||
artifacts and an EC-3 `worktree_path=` row. On this flow they read `INDETERMINATE`
|
||||
(plan lock reads `false`), and `q4_records` says once for the mission that no
|
||||
build is recorded. Built means a Git commit, so work that isn't a commit never
|
||||
reads built.
|
||||
|
||||
When the owning scope selects `proofPolicy.judges`, use `rig proof judge` to
|
||||
record an attributed accept, reject or withdraw decision on a contract item,
|
||||
and `rig proof show` to read derived readiness up through slice, mission and
|
||||
|
||||
@@ -18,6 +18,8 @@ import { isTypedGateBlocker } from "./queue-repository.js";
|
||||
// "INDETERMINATE" (with a named basis), never idle/dead/done/false.
|
||||
// - THE LADDER IS THE SCHEMA: done-ness is five named rungs
|
||||
// (locked/built/reviewed/folded/adopted); a single "done" boolean does not exist.
|
||||
// What a team recorded about a slice (its `status:` word, a `merged:<pr>` report)
|
||||
// is shown beside the rungs as labelled testimony and never sets one.
|
||||
// - PROJECTION, NOT AUTHORITY: every cell carries the row/artifact/command that
|
||||
// makes it one command from source.
|
||||
//
|
||||
@@ -38,7 +40,7 @@ import { promisify } from "node:util";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import type Database from "better-sqlite3";
|
||||
import { shellQuote } from "../adapters/shell-quote.js";
|
||||
import { parseFrontmatter } from "./slices/slice-indexer.js";
|
||||
import { parseFrontmatter, sliceStatusBucket } from "./slices/slice-indexer.js";
|
||||
import { lastMeaningfulTransition } from "./queue-waiting.js";
|
||||
import { derivePickup } from "./queue-pickup.js";
|
||||
|
||||
@@ -986,7 +988,13 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
|
||||
const fm = facts.frontmatter;
|
||||
const locked: Rung = typeof fm["approved-spec-at"] === "string"
|
||||
? { value: true, basis: `frontmatter approved-spec-at=${fm["approved-spec-at"]}` }
|
||||
: { value: false, basis: "no approved-spec-at in frontmatter" };
|
||||
: { value: false, basis: "no approved-spec-at in frontmatter; plan lock is a Part B step (rig scope slice approve --scope spec), so the default flow reads false here whatever the recorded status" };
|
||||
// The team's own word for the slice, as `GET /api/slices` buckets it. Testimony: it never sets a rung or next-up.
|
||||
const rawStatus = fm["status"];
|
||||
const statusWord = typeof rawStatus === "string" && rawStatus.trim() ? rawStatus.trim() : null;
|
||||
const recorded_status = statusWord
|
||||
? { word: statusWord, bucket: sliceStatusBucket(statusWord), basis: "slice frontmatter status:, recorded by the team, not verified; it never sets a rung" }
|
||||
: { word: null, bucket: null, basis: "no status: in slice frontmatter" };
|
||||
// built: latest candidate:<sha> tag on a row bound to this slice.
|
||||
const candRows = rows
|
||||
.filter((r) => sliceOfRow(r) === facts.id || sliceOfRow(r) === facts.dir)
|
||||
@@ -1110,7 +1118,7 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
|
||||
: buildInfo.commit
|
||||
? await gitAncestor(exec, repoCtx, builtResolved ?? candidateSha, buildInfo.commit)
|
||||
: { value: INDETERMINATE, basis: "daemon build stamp absent (dev run) — adopted rung underivable" };
|
||||
return { slice_id: facts.id, dir: facts.dir, locked, built, reviewed, folded, adopted };
|
||||
return { slice_id: facts.id, dir: facts.dir, recorded_status, locked, built, reviewed, folded, adopted };
|
||||
};
|
||||
|
||||
const ladderCache = new Map<string, Record<string, unknown>>();
|
||||
@@ -1124,6 +1132,20 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
|
||||
// so mutable refs (HEAD/main) are observed again on the next request.
|
||||
const q4: Record<string, unknown>[] = [];
|
||||
for (const s of slices) q4.push(await ladderFor(s));
|
||||
// #2761: say once what the mission records for the ladder, so missing records never read as missing work.
|
||||
const withCandidate = q4.filter((l) => (l["built"] as { candidate_sha?: unknown }).candidate_sha !== INDETERMINATE).length;
|
||||
const withMergeReport = q4.filter((l) => !!(l["folded"] as { merge_report?: unknown }).merge_report).length;
|
||||
const q4Records = {
|
||||
slices: q4.length,
|
||||
with_candidate: withCandidate,
|
||||
with_merge_report: withMergeReport,
|
||||
repo_context: repoCtx ?? INDETERMINATE,
|
||||
basis: q4.length === 0
|
||||
? "no slices in this mission"
|
||||
: withCandidate === 0
|
||||
? "no slice in this mission has a candidate:<sha> row, so built, reviewed and merged read INDETERMINATE: not recorded, which is not the same as not done. The default flow (sdlc-conventions.md Part A) doesn't record them; each slice's recorded_status is the team's own word"
|
||||
: `${withCandidate} of ${q4.length} slices have a candidate:<sha> row; ${repoCtx ? `merged is tested in repo context ${repoCtx}` : "no row carries worktree_path= (EC-3), so merged can't be tested"}`,
|
||||
};
|
||||
|
||||
// ---- Q2 sequencing ----
|
||||
const q2 = [];
|
||||
@@ -1382,6 +1404,7 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
|
||||
q2_sequencing: q2,
|
||||
q3_care: q3,
|
||||
q4_ladder: q4,
|
||||
q4_records: q4Records,
|
||||
q5_park: q5,
|
||||
q6_parallelism: q6,
|
||||
};
|
||||
|
||||
@@ -266,6 +266,18 @@ const STATUS_TO_BUCKET: Record<string, SliceStatus> = {
|
||||
"parked-with-evidence": "blocked",
|
||||
};
|
||||
|
||||
/** The bucket a slice's frontmatter `status:` word falls in, as `GET /api/slices` reports it (sdlc-conventions.md A2). */
|
||||
export function sliceStatusBucket(raw: string | undefined): SliceStatus {
|
||||
if (!raw) return "draft";
|
||||
const normalized = raw.toLowerCase().trim();
|
||||
if (STATUS_TO_BUCKET[normalized]) return STATUS_TO_BUCKET[normalized];
|
||||
// Heuristic fallbacks.
|
||||
if (normalized.includes("done") || normalized.includes("ship") || normalized.includes("close")) return "done";
|
||||
if (normalized.includes("block") || normalized.includes("park")) return "blocked";
|
||||
if (normalized.includes("draft") || normalized.includes("pending")) return "draft";
|
||||
return "active";
|
||||
}
|
||||
|
||||
export class SliceIndexer {
|
||||
readonly projectId?: string;
|
||||
readonly missionId?: string;
|
||||
@@ -765,14 +777,7 @@ export class SliceIndexer {
|
||||
}
|
||||
|
||||
private mapStatus(raw: string | undefined): SliceStatus {
|
||||
if (!raw) return "draft";
|
||||
const normalized = raw.toLowerCase().trim();
|
||||
if (STATUS_TO_BUCKET[normalized]) return STATUS_TO_BUCKET[normalized];
|
||||
// Heuristic fallbacks.
|
||||
if (normalized.includes("done") || normalized.includes("ship") || normalized.includes("close")) return "done";
|
||||
if (normalized.includes("block") || normalized.includes("park")) return "blocked";
|
||||
if (normalized.includes("draft") || normalized.includes("pending")) return "draft";
|
||||
return "active";
|
||||
return sliceStatusBucket(raw);
|
||||
}
|
||||
|
||||
private matchQitems(sliceName: string, served: Set<string> | null): string[] {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// #2761: on the default flow (sdlc-conventions Part A) a team records a slice's `status:`, not plan lock, `candidate:`
|
||||
// rows, review artifacts or an EC-3 worktree row. The execution view must say what it couldn't read instead of reading
|
||||
// finished work as not done: each slice shows its recorded status beside the ladder (the team's word, never a rung), and
|
||||
// the mission says once that no build records exist. The rungs and next-up stay exactly as their own records give them.
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type Database from "better-sqlite3";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
|
||||
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
|
||||
import { eventsSchema } from "../src/db/migrations/003_events.js";
|
||||
import { queueItemSummarySchema } from "../src/db/migrations/044_queue_item_summary.js";
|
||||
import { queueItemsSchema } from "../src/db/migrations/024_queue_items.js";
|
||||
import { queueTransitionsSchema } from "../src/db/migrations/025_queue_transitions.js";
|
||||
import { queueTransitionWakesSchema } from "../src/db/migrations/073_queue_transition_wakes.js";
|
||||
import { viewsCustomSchema } from "../src/db/migrations/030_views_custom.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
import { ViewProjector } from "../src/domain/view-projector.js";
|
||||
|
||||
const MISSION = "simple-flow";
|
||||
type Doc = Record<string, unknown>;
|
||||
type Entry = Record<string, Record<string, unknown>> & { slice_id: string };
|
||||
|
||||
describe("#2761 execution view on the default flow", () => {
|
||||
let db: Database.Database;
|
||||
let projector: ViewProjector;
|
||||
let tmp: string;
|
||||
let missionsRoot: string;
|
||||
|
||||
const slice = (dir: string, frontmatter: string[]) => {
|
||||
const d = path.join(missionsRoot, MISSION, "slices", dir);
|
||||
fs.mkdirSync(d, { recursive: true });
|
||||
fs.writeFileSync(path.join(d, "SPEC.md"), `---\n${frontmatter.join("\n")}\n---\n\n## Intent\n${dir}\n`);
|
||||
};
|
||||
const row = (id: string, tags: string[], body = "Work.") =>
|
||||
db.prepare(`INSERT INTO queue_items (qitem_id, ts_created, ts_updated, source_session, destination_session, state, priority, tier, tags, body)
|
||||
VALUES (?, '2026-11-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z', 'lead@t', 'builder@t', 'done', 'normal', 'light', ?, ?)`)
|
||||
.run(id, JSON.stringify(tags), body);
|
||||
const show = async (): Promise<Doc> => (await projector.show("execution", { mission: MISSION })).rows[0] as Doc;
|
||||
const ladder = (doc: Doc) => doc.q4_ladder as Entry[];
|
||||
const entry = (doc: Doc, id: string) => ladder(doc).find((e) => e.slice_id === id)!;
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "exec-default-flow-"));
|
||||
missionsRoot = path.join(tmp, "missions");
|
||||
fs.mkdirSync(path.join(missionsRoot, MISSION), { recursive: true });
|
||||
fs.writeFileSync(path.join(missionsRoot, MISSION, "SPEC.md"), `---\nid: SF\nmission: ${MISSION}\n---\n# Simple flow\n`);
|
||||
// Finished on the default flow: the team's own words, as `rig scope slice ship`/`close` and hand edits write them.
|
||||
slice("01-done", ["id: SF.1", "status: done", "depends_on: []"]);
|
||||
slice("02-shipped", ["id: SF.2", "status: shipped-to-release-1", 'depends_on: ["SF.1"]']);
|
||||
slice("03-closed", ["id: SF.3", "status: Closed", "depends_on: []"]);
|
||||
// Scaffolded after the work, with no status at all (the reporter's 9-slice mission).
|
||||
slice("04-unrecorded", ["id: SF.4", "depends_on: []"]);
|
||||
db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, queueItemsSchema, queueItemSummarySchema,
|
||||
queueTransitionsSchema, queueTransitionWakesSchema, viewsCustomSchema]);
|
||||
projector = new ViewProjector(db, new EventBus(db));
|
||||
projector.setExecutionDeps({
|
||||
db,
|
||||
slicesRoot: () => missionsRoot,
|
||||
rigsRoot: () => path.join(tmp, "rigs"),
|
||||
buildInfo: { semver: null, commit: null, dirty: null, builtAt: null },
|
||||
now: () => new Date("2026-11-10T00:00:00.000Z"),
|
||||
seatActivity: { getSeatStateBySession: () => null },
|
||||
} as never);
|
||||
// The team's rows name the mission, as Part A asks; none carries candidate: or worktree_path=.
|
||||
row("qitem-sf-1", [`mission:${MISSION}`, "slice:01-done"]);
|
||||
row("qitem-sf-2", [`mission:${MISSION}`, "slice:build-two"]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("shows each slice's recorded status and its bucket beside the ladder, as testimony", async () => {
|
||||
const doc = await show();
|
||||
expect(entry(doc, "SF.1").recorded_status).toMatchObject({ word: "done", bucket: "done" });
|
||||
expect(entry(doc, "SF.2").recorded_status).toMatchObject({ word: "shipped-to-release-1", bucket: "done" });
|
||||
expect(entry(doc, "SF.3").recorded_status).toMatchObject({ word: "Closed", bucket: "done" });
|
||||
expect(entry(doc, "SF.1").recorded_status!.basis).toMatch(/recorded by the team, not verified; it never sets a rung/);
|
||||
// No status recorded is said as such, not read as draft or as not done.
|
||||
expect(entry(doc, "SF.4").recorded_status).toEqual({ word: null, bucket: null, basis: "no status: in slice frontmatter" });
|
||||
});
|
||||
|
||||
it("says once for the mission that nothing records a build, so built and merged read not recorded", async () => {
|
||||
const records = (await show()).q4_records as Record<string, unknown>;
|
||||
expect(records).toMatchObject({ slices: 4, with_candidate: 0, with_merge_report: 0, repo_context: "INDETERMINATE" });
|
||||
expect(records.basis).toMatch(/^no slice in this mission has a candidate:<sha> row/);
|
||||
expect(records.basis).toMatch(/not recorded, which is not the same as not done/);
|
||||
});
|
||||
|
||||
it("keeps every rung and next-up as their own records give them: a recorded status never turns one", async () => {
|
||||
const doc = await show();
|
||||
for (const e of ladder(doc)) {
|
||||
expect(e.built!.candidate_sha).toBe("INDETERMINATE");
|
||||
expect(e.reviewed!.value).toBe("INDETERMINATE");
|
||||
expect(e.folded!.value).toBe("INDETERMINATE");
|
||||
}
|
||||
// Plan lock is a Part B step: false stays false, and the basis says why it reads false on this flow.
|
||||
expect(entry(doc, "SF.1").locked).toEqual({
|
||||
value: false,
|
||||
basis: "no approved-spec-at in frontmatter; plan lock is a Part B step (rig scope slice approve --scope spec), so the default flow reads false here whatever the recorded status",
|
||||
});
|
||||
// Next-up reads the folded rung, not the status: a done-recorded slice stays INDETERMINATE, never dispatchable.
|
||||
const q2 = doc.q2_sequencing as Array<Record<string, unknown>>;
|
||||
expect(q2.find((s) => s.slice_id === "SF.1")).toMatchObject({ next_up: "INDETERMINATE" });
|
||||
expect(q2.find((s) => s.slice_id === "SF.4")).toMatchObject({ next_up: "INDETERMINATE" });
|
||||
});
|
||||
|
||||
it("counts recorded builds and merge reports when a team does record them", async () => {
|
||||
row("qitem-sf-built", [`mission:${MISSION}`, "slice:01-done", "candidate:0123456789abcdef0123456789abcdef01234567"]);
|
||||
row("qitem-sf-merged", [`mission:${MISSION}`, "slice:03-closed", "merged:41"]);
|
||||
const records = (await show()).q4_records as Record<string, unknown>;
|
||||
expect(records).toMatchObject({ slices: 4, with_candidate: 1, with_merge_report: 1, repo_context: "INDETERMINATE" });
|
||||
expect(records.basis).toMatch(/^1 of 4 slices have a candidate:<sha> row; no row carries worktree_path= \(EC-3\), so merged can't be tested/);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,8 @@ export interface ExecutionViewSnap {
|
||||
q2_sequencing: Array<Record<string, unknown>>;
|
||||
q3_care?: Array<Record<string, unknown>>;
|
||||
q4_ladder: Array<Record<string, unknown>>;
|
||||
/** #2761: what the mission records for the ladder (absent on older daemons). */
|
||||
q4_records?: Record<string, unknown>;
|
||||
q5_park: Array<Record<string, unknown>>;
|
||||
q6_parallelism?: Record<string, unknown>;
|
||||
/** S06: existing workflow engine facts joined to the selected mission. */
|
||||
@@ -220,6 +222,13 @@ function declaredText(slice: SliceFacts): string {
|
||||
return slice.scope?.status?.trim().toLowerCase() || "no declared status";
|
||||
}
|
||||
|
||||
/** A declared word in the done bucket (done, shipped, promoted, closed…), as the daemon buckets it beside the ladder;
|
||||
* an older daemon serves no bucket, and only the word "done" counts. */
|
||||
function declaredDone(ladder: Record<string, unknown>, word: string): boolean {
|
||||
const bucket = record(ladder["recorded_status"])["bucket"];
|
||||
return typeof bucket === "string" ? bucket === "done" : word === "done";
|
||||
}
|
||||
|
||||
function seatShort(seat: unknown): string {
|
||||
const full = str(seat, "");
|
||||
return full.includes("@") ? full.slice(0, full.indexOf("@")) : full;
|
||||
@@ -267,7 +276,7 @@ function stateWord(slice: SliceFacts): string {
|
||||
if (slice.readiness?.configured) return "outcomes pending";
|
||||
if (stageText(slice) === "retired" || declaredText(slice) === "retired") return "retired";
|
||||
if (declaredText(slice) === "deferred" || declaredText(slice) === "closed-deferred") return "deferred";
|
||||
return declaredText(slice) === "done" ? "declared done" : "planned";
|
||||
return declaredDone(slice.ladder, declaredText(slice)) ? "declared done" : "planned";
|
||||
}
|
||||
|
||||
function proofText(scope: SliceScopeSnap | null): string | null {
|
||||
@@ -426,6 +435,7 @@ function collectIndeterminate(execution: ExecutionViewSnap, slices: SliceFacts[]
|
||||
function evidenceDetail(execution: ExecutionViewSnap, slices: SliceFacts[], width: number, timeZone = DEFAULT_TIME_ZONE): ContentLine[] {
|
||||
const attributed = slices.filter(slice => slice.readiness?.configured);
|
||||
const gitBasis = str(record(execution.sources?.["git"])["basis"], "(no git source cell)");
|
||||
const recordsBasis = record(execution.q4_records)["basis"];
|
||||
const lines: ContentLine[] = [
|
||||
{ text: `${execution.mission} · ${attributed.length ? "proof provenance" : "evidence gap"} · derived ${displayTime(execution.derived_at, timeZone) || "?"}` },
|
||||
{ text: "" },
|
||||
@@ -441,6 +451,7 @@ function evidenceDetail(execution: ExecutionViewSnap, slices: SliceFacts[], widt
|
||||
}
|
||||
lines.push({ text: "" }, sectionRule("code lineage · separate from item judgments", width),
|
||||
{ text: ` git: ${gitBasis}` },
|
||||
...(typeof recordsBasis === "string" ? wrapDetailLines([{ text: ` records: ${recordsBasis}` }], width) : []),
|
||||
...wrapDetailLines([{ text: " Build, review and merge facts remain on each slice's code evidence. Daemon adoption applies only to the unscoped daemon-source view. Artifact acceptance supplies none of these code facts." }], width));
|
||||
for (const item of collectIndeterminate(execution, slices)) {
|
||||
lines.push({ text: "" }, sectionRule(`${item.where} unconfirmed for ${item.members.length} slice${item.members.length === 1 ? "" : "s"}`, width));
|
||||
@@ -845,7 +856,7 @@ export function executionSliceStripLines(
|
||||
const evidence = `${evidenceText(slice.cells, slice.rank)}${unconfirmed.length ? ` · ${unconfirmed.join(" / ")} unconfirmed (${slice.cells[RUNGS.find((rung) => slice.cells[rung].state === "undetermined")!].basis})` : ""}`;
|
||||
const liveWord = slice.lane ? (waitingOn(slice.lane) ? "waiting" : str(activity["activity"], "claimed")) : "no claimed lane";
|
||||
const declaredWord = declared?.trim().toLowerCase() || "no declared status";
|
||||
const next = (declaredWord === "done" || declaredWord === "retired" || declaredWord === "deferred" || declaredWord === "closed-deferred") && !slice.lane
|
||||
const next = (declaredWord === "retired" || declaredWord === "deferred" || declaredWord === "closed-deferred" || declaredDone(slice.ladder, declaredWord)) && !slice.lane
|
||||
? `none — declared ${declaredWord}`
|
||||
: nextText(slice) ?? (slice.lane ? (waitingOn(slice.lane) ? `waiting on ${waitingOn(slice.lane)}, see the waiting row above` : "in progress on the lane above") : "nothing the projection can sequence");
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// #2761: on the default flow a team records each slice's `status:` word (`rig scope slice ship` writes
|
||||
// `shipped-to-<mission>`, `close` writes `closed-<reason>`) and no build records. The mission view must read every word
|
||||
// in the done bucket as declared done, not "planned", and say once that the ladder's build records weren't kept.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { executionContentLines, executionSliceStripLines } from "../src/execution/execution-model.js";
|
||||
import { demoSnapshot } from "../src/demo-data.js";
|
||||
import type { FleetSnapshot } from "../src/types.js";
|
||||
|
||||
const WORDS = ["shipped-to-release-1", "closed", "promoted"];
|
||||
const RECORDS = "no slice in this mission has a candidate:<sha> row, so built, reviewed and merged read INDETERMINATE: not recorded, which is not the same as not done";
|
||||
const UNRECORDED = "no candidate:* tag on any row bound to this slice (unbuilt and unrecorded are indistinguishable here)";
|
||||
|
||||
/** A finished default-flow mission. `served` is a daemon that serves recorded_status and q4_records; without it, an older one. */
|
||||
function finished(served: boolean) {
|
||||
const ids = WORDS.map((_, i) => `SF.${i + 1}`);
|
||||
const dir = (i: number) => `0${i + 1}-slice`;
|
||||
const execution = {
|
||||
view: "execution",
|
||||
mission: "simple-flow",
|
||||
derived_at: "2026-11-10T00:00:00.000Z",
|
||||
sources: { git: { basis: "no reachable repo context" }, build_info: { commit: null } },
|
||||
q1_lanes: [],
|
||||
q2_sequencing: ids.map((id, i) => ({ slice_id: id, dir: dir(i), depends_on: [], blocked_on_rows: [], next_up: "INDETERMINATE",
|
||||
next_up_basis: "own completion rung INDETERMINATE (no candidate sha to test)", next_up_rank: null })),
|
||||
q3_care: ids.map((id) => ({ slice_id: id, build_wave: "w1" })),
|
||||
q4_ladder: ids.map((id, i) => ({
|
||||
slice_id: id,
|
||||
dir: dir(i),
|
||||
...(served ? { recorded_status: { word: WORDS[i], bucket: "done", basis: "slice frontmatter status:, recorded by the team, not verified; it never sets a rung" } } : {}),
|
||||
locked: { value: false, basis: "no approved-spec-at in frontmatter" },
|
||||
built: { candidate_sha: "INDETERMINATE", basis: UNRECORDED },
|
||||
reviewed: { value: "INDETERMINATE", basis: "no built candidate token to scope review legs to", legs: [] },
|
||||
folded: { value: "INDETERMINATE", basis: "no candidate sha to test" },
|
||||
adopted: { value: "INDETERMINATE", basis: "no candidate sha to test" },
|
||||
})),
|
||||
...(served ? { q4_records: { slices: 3, with_candidate: 0, with_merge_report: 0, repo_context: "INDETERMINATE", basis: RECORDS } } : {}),
|
||||
q5_park: [],
|
||||
} as NonNullable<FleetSnapshot["execution"]>;
|
||||
const template = demoSnapshot().scopes![0]!.slices[0]!;
|
||||
const scopes = [{ mission: "simple-flow", slices: ids.map((id, i) => ({ ...template, id, dirName: dir(i), displayName: `Slice ${i + 1}`, status: WORDS[i], proof: { paired: 0, total: 0 } })) }];
|
||||
return { execution, scopes };
|
||||
}
|
||||
const text = (lines: ReturnType<typeof executionContentLines>) => lines.map((line) => line.text).join("\n");
|
||||
|
||||
describe("#2761 execution view on the default flow", () => {
|
||||
it("reads every done-bucket word as declared done, not planned", () => {
|
||||
const { execution, scopes } = finished(true);
|
||||
const body = text(executionContentLines(execution, scopes, [], null, 160));
|
||||
expect(body).toContain("WAVE w1 · 3 slices · 3 declared done");
|
||||
expect(body).not.toContain("planned");
|
||||
});
|
||||
|
||||
it("the slice strip has nothing next for a slice declared shipped", () => {
|
||||
const { execution } = finished(true);
|
||||
const strip = text(executionSliceStripLines(execution, "SF.1", "01-slice", 200, "shipped-to-release-1"));
|
||||
expect(strip).toContain("next none — declared shipped-to-release-1");
|
||||
});
|
||||
|
||||
it("the evidence page says once that the mission records no builds", () => {
|
||||
const { execution, scopes } = finished(true);
|
||||
const page = text(executionContentLines(execution, scopes, [], "evidence", 400));
|
||||
expect(page).toContain(`records: ${RECORDS}`);
|
||||
});
|
||||
|
||||
it("an older daemon that serves no bucket keeps reading only the word done as declared done", () => {
|
||||
const { execution, scopes } = finished(false);
|
||||
const body = text(executionContentLines(execution, scopes, [], null, 160));
|
||||
expect(body).toContain("WAVE w1 · 3 slices · 3 planned");
|
||||
expect(text(executionContentLines(execution, scopes, [], "evidence", 400))).not.toContain("records:");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user