-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevflow.test.ts
More file actions
160 lines (150 loc) · 8.63 KB
/
Copy pathdevflow.test.ts
File metadata and controls
160 lines (150 loc) · 8.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const SOURCE_ROOT = join(import.meta.dir, "..");
const CHANGE_ID = "2026-09-08-four-stage-smoke";
function withRepository(check: (root: string) => void): void {
const root = mkdtempSync(join(tmpdir(), "codetwo-devflow-"));
try {
mkdirSync(join(root, "script"), { recursive: true });
cpSync(join(SOURCE_ROOT, "script/devflow.ts"), join(root, "script/devflow.ts"));
cpSync(join(SOURCE_ROOT, "script/verify"), join(root, "script/verify"), { recursive: true });
mkdirSync(join(root, "docs/sdlc/changes"), { recursive: true });
cpSync(join(SOURCE_ROOT, ".agents/skills"), join(root, ".agents/skills"), { recursive: true });
writeFileSync(join(root, ".agents/skills/codetwo-develop/references/workflow.md"), "# Fixture workflow\n");
check(root);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
function run(root: string, args: string[], env: Record<string, string> = {}) {
const result = spawnSync("bun", ["script/devflow.ts", ...args], {
cwd: root, encoding: "utf8", env: { ...process.env, GITHUB_EVENT_PATH: "", DEVFLOW_DATE: "2026-09-08", ...env },
});
return { status: result.status, output: result.stdout + result.stderr };
}
function git(root: string, ...args: string[]): string {
const r = spawnSync("git", args, { cwd: root, encoding: "utf8" });
if (r.status !== 0) throw new Error(r.stderr);
return r.stdout.trim();
}
function acceptStages(root: string): string {
const dir = join(root, "docs/sdlc/changes", CHANGE_ID);
for (const stage of ["intent", "spec", "plan"]) {
const path = join(dir, `${stage}.md`);
const accepted = readFileSync(path, "utf8")
.replaceAll("[fill]", "Concrete fixture requirement.")
.replace("owner: unassigned", "owner: fixture")
.replace("scope: pending", "scope: README.md")
.replace('approved_by: ""', 'approved_by: "requester"')
.replace('approved_at: ""', 'approved_at: "2026-09-08"')
.replace('approval_source: ""', 'approval_source: "Fixture user explicitly requested this bounded change."')
.replace("status: draft", "status: accepted");
writeFileSync(path, accepted);
}
return readFileSync(join(dir, "verification.md"), "utf8");
}
function finishCleanup(text: string): string {
return text.replace("cleanup_status: pending", "cleanup_status: complete")
.replace("Removed: pending.", "Removed: none; disposable fixture only.")
.replace("Retained: pending.", "Retained: none.")
.replace("Processes: pending.", "Processes: none started.")
.replace("Evidence: pending.", "Evidence: `fixture` removes its temporary directory in finally.");
}
function passStages(root: string, pending: string): string {
const path = join(root, "docs/sdlc/changes", CHANGE_ID, "spec.md");
writeFileSync(path, readFileSync(path, "utf8").replace("[ ] AC-1", "[x] AC-1"));
return finishCleanup(pending).replace("status: pending", "status: passed")
.replace("owner: unassigned", "owner: fixture")
.replace('revision: ""', 'revision: "disposable worktree fixture"')
.replace('verified_by: ""', 'verified_by: "fixture"')
.replace('verified_at: ""', 'verified_at: "2026-09-08"')
.replace("AC-1: BLOCKED — Acceptance has not been checked.", "AC-1: PASS — `fixture-check` passed in disposable worktree.")
.replace("Verdict: pending.", "Verdict: verified.")
.replace("Residual risk: pending.", "Residual risk: disposable fixture only.");
}
test("four files run request, local work, honest failure, verification and Ready PR without stage approval commands", () => {
withRepository(root => {
expect(run(root, ["new", "four-stage-smoke", "user", "low"]).status).toBe(0);
const path = join(root, "docs/sdlc/changes", CHANGE_ID, "verification.md");
expect(existsSync(join(root, "docs/sdlc/changes", CHANGE_ID, "intent.md"))).toBe(true);
expect(existsSync(join(root, "docs/sdlc/changes", CHANGE_ID, "change.md"))).toBe(false);
expect(run(root, ["validate"]).status).toBe(0);
expect(readFileSync(path, "utf8")).toContain("cleanup_status: pending");
expect(readFileSync(path, "utf8")).toContain("## Cleanup");
const draft = { PR_BODY: `Change: docs/sdlc/changes/${CHANGE_ID}/intent.md`, PR_IS_DRAFT: "true" };
expect(run(root, ["check-pr"], draft).status).toBe(0);
expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).output).toContain("intent must be accepted");
const accepted = acceptStages(root);
writeFileSync(path, accepted);
expect(run(root, ["validate"]).status).toBe(0);
expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).output).toContain("verification passed");
writeFileSync(path, finishCleanup(accepted).replace("status: pending", "status: failed").replace("owner: unassigned", "owner: fixture")
.replace("AC-1: BLOCKED — Acceptance has not been checked.", "AC-1: FAIL — `fixture-check` failed.")
.replace("Verdict: pending.", "Verdict: failed."));
expect(run(root, ["check-pr"], draft).status).toBe(0);
expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).status).not.toBe(0);
writeFileSync(path, passStages(root, accepted));
expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).status).toBe(0);
expect(run(root, ["status", CHANGE_ID]).output).toContain("verification.md=passed");
writeFileSync(path, passStages(root, accepted).replace("cleanup_status: complete\n", ""));
expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).output).toContain("requires cleanup_status");
});
});
test("CI event checks every changed record and scopes implementation; PR text is data", () => {
withRepository(root => {
git(root, "init", "-q");
git(root, "config", "user.name", "Fixture");
git(root, "add", ".");
git(root, "commit", "-qm", "baseline");
const base = git(root, "rev-parse", "HEAD");
run(root, ["new", "four-stage-smoke", "user", "medium"]);
const path = join(root, "docs/sdlc/changes", CHANGE_ID, "verification.md");
const accepted = acceptStages(root);
writeFileSync(path, accepted);
writeFileSync(join(root, "README.md"), "Changed fixture.\n");
git(root, "add", "."); git(root, "commit", "-qm", "in progress");
const eventPath = join(root, "event.json");
const event = { pull_request: { base: { sha: base }, draft: true,
body: `Change: docs/sdlc/changes/${CHANGE_ID}/intent.md\n$(touch should-not-exist)` } };
const check = () => {
writeFileSync(eventPath, JSON.stringify(event));
return run(root, ["check-pr"], { GITHUB_EVENT_PATH: eventPath });
};
expect(check().status).toBe(0);
event.pull_request.draft = false;
expect(check().output).toContain("verification passed");
writeFileSync(path, passStages(root, accepted));
git(root, "add", "docs"); git(root, "commit", "-qm", "verified");
expect(check().status).toBe(0);
expect(existsSync(join(root, "should-not-exist"))).toBe(false);
event.pull_request.base.sha = "";
expect(check().output).toContain("requires a base SHA");
event.pull_request.base.sha = base;
run(root, ["new", "second-record", "user", "low"]);
git(root, "add", "docs"); git(root, "commit", "-qm", "second draft");
expect(check().output).toContain("Ready PR");
event.pull_request.draft = true;
expect(check().output).toContain("must link changed record");
event.pull_request.body += "\nChange: docs/sdlc/changes/2026-09-08-second-record/intent.md";
expect(check().status).toBe(0);
writeFileSync(join(root, "uncovered.txt"), "Uncovered implementation\n");
git(root, "add", "uncovered.txt"); git(root, "commit", "-qm", "out of scope");
expect(check().output).toContain("not covered");
});
});
test("incident creates one follow-up and links it, without touching runtime state", () => {
withRepository(root => {
expect(run(root, ["incident", "provider-recovery", "monitor"]).status).toBe(0);
const record = join(root, "docs/sdlc/changes/2026-09-08-provider-recovery/intent.md");
expect(existsSync(record)).toBe(true);
expect(readFileSync(record, "utf8")).toContain("risk: high");
const incident = readFileSync(join(root, "docs/sdlc/incidents/2026-09-08-provider-recovery.md"), "utf8");
expect(incident).toContain("../changes/2026-09-08-provider-recovery/intent.md");
expect(run(root, ["validate"]).status).toBe(0);
expect(run(root, ["incident", "provider-recovery", "monitor"]).status).not.toBe(0);
});
});