@izkac/forgekit 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const E2E = path.join(path.dirname(fileURLToPath(import.meta.url)), 'e2e.mjs');
|
|
10
|
+
|
|
11
|
+
function tmp(prefix) {
|
|
12
|
+
return fs.mkdtempSync(path.join(tmpdir(), prefix));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function run(cwd, args) {
|
|
16
|
+
return execFileSync(process.execPath, [E2E, ...args], {
|
|
17
|
+
cwd,
|
|
18
|
+
env: { ...process.env, FORGEKIT_FLEET_DIR: path.join(tmp('e2e-fleet-'), 's') },
|
|
19
|
+
}).toString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** .forge fixture with an active session tracking a specs change. */
|
|
23
|
+
function makeFixture(root) {
|
|
24
|
+
const sessionDir = path.join(root, '.forge', 'sessions', 's1');
|
|
25
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(sessionDir, 'session.json'),
|
|
28
|
+
`${JSON.stringify({ id: 's1', slug: 'fixture', planType: 'specs', openspecChange: 'my-change' })}\n`,
|
|
29
|
+
'utf8',
|
|
30
|
+
);
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(root, '.forge', 'active.json'),
|
|
33
|
+
`${JSON.stringify({ sessionId: 's1' })}\n`,
|
|
34
|
+
'utf8',
|
|
35
|
+
);
|
|
36
|
+
fs.mkdirSync(path.join(root, 'specs', 'changes', 'my-change'), { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('e2e harness: record → show → surfaced by init; config keys preserved', () => {
|
|
40
|
+
const root = tmp('e2e-harness-');
|
|
41
|
+
makeFixture(root);
|
|
42
|
+
// Pre-existing config keys must survive the harness merge-write.
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(root, '.forge', 'config.json'),
|
|
45
|
+
`${JSON.stringify({ plan: { engine: 'specs', dir: 'specs' } }, null, 2)}\n`,
|
|
46
|
+
'utf8',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assert.match(run(root, ['harness']), /No harness recorded/);
|
|
50
|
+
|
|
51
|
+
run(root, [
|
|
52
|
+
'harness',
|
|
53
|
+
'--set',
|
|
54
|
+
'compose test stack: server + scratch mongo on isolated ports',
|
|
55
|
+
'--start',
|
|
56
|
+
'npm run e2e:stack',
|
|
57
|
+
'--dir',
|
|
58
|
+
'scripts/e2e',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, '.forge', 'config.json'), 'utf8'));
|
|
62
|
+
assert.equal(cfg.plan.engine, 'specs');
|
|
63
|
+
assert.equal(cfg.e2e.harness.start, 'npm run e2e:stack');
|
|
64
|
+
assert.match(cfg.e2e.harness.description, /compose test stack/);
|
|
65
|
+
|
|
66
|
+
assert.match(run(root, ['harness']), /REUSE it — do not build/);
|
|
67
|
+
assert.match(run(root, ['init']), /REUSE it — do not build/);
|
|
68
|
+
assert.equal(JSON.parse(run(root, ['status'])).harness.dir, 'scripts/e2e');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('e2e harness --set requires a description', () => {
|
|
72
|
+
const root = tmp('e2e-harness-req-');
|
|
73
|
+
makeFixture(root);
|
|
74
|
+
assert.throws(() => run(root, ['harness', '--set']), /Usage: forge e2e harness --set/);
|
|
75
|
+
});
|
package/src/e2e.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* forge e2e init [--force] # scaffold e2e.json for the active change
|
|
8
8
|
* forge e2e run # execute steps, write e2e-results.json (session dir)
|
|
9
9
|
* forge e2e check # gate check: green + current results; exit 1 with problems
|
|
10
|
+
* forge e2e harness # show recorded project harness (reuse it!)
|
|
11
|
+
* forge e2e harness --set "<desc>" [--start "<cmd>"] [--dir <path>]
|
|
10
12
|
* [--session <id>]
|
|
11
13
|
*
|
|
12
14
|
* e2e.json lives next to spine.json (change dir, falling back to the session
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
|
|
18
20
|
import fs from 'node:fs';
|
|
19
21
|
import { loadSession, readActive, readJson } from './lib.mjs';
|
|
22
|
+
import { loadProjectConfig, saveProjectConfig } from './config.mjs';
|
|
20
23
|
import {
|
|
21
24
|
checkE2eGate,
|
|
22
25
|
e2ePath,
|
|
@@ -34,11 +37,57 @@ const sub = args[0] && !args[0].startsWith('--') ? args[0] : 'status';
|
|
|
34
37
|
|
|
35
38
|
if (args[0] === '--help' || sub === 'help') {
|
|
36
39
|
process.stdout.write(
|
|
37
|
-
'Usage: forge e2e [init [--force] | run | check | status] [--session <id>]\n',
|
|
40
|
+
'Usage: forge e2e [init [--force] | run | check | status | harness [--set <desc> --start <cmd> --dir <path>]] [--session <id>]\n',
|
|
38
41
|
);
|
|
39
42
|
process.exit(0);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/** Recorded project harness (committed in .forge/config.json → e2e.harness). */
|
|
46
|
+
function loadHarness() {
|
|
47
|
+
const cfg = loadProjectConfig(process.cwd());
|
|
48
|
+
const h = cfg?.e2e?.harness;
|
|
49
|
+
return h && typeof h === 'object' ? h : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function harnessLines(h) {
|
|
53
|
+
const lines = [`Existing e2e harness (REUSE it — do not build or ask for a new one):`];
|
|
54
|
+
lines.push(` ${h.description}`);
|
|
55
|
+
if (h.start) lines.push(` Start: ${h.start}`);
|
|
56
|
+
if (h.dir) lines.push(` Location: ${h.dir}`);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Project-level, no session needed.
|
|
61
|
+
if (sub === 'harness') {
|
|
62
|
+
const si = args.indexOf('--set');
|
|
63
|
+
if (si >= 0) {
|
|
64
|
+
const description = args[si + 1];
|
|
65
|
+
if (!description || description.startsWith('--')) {
|
|
66
|
+
process.stderr.write('Usage: forge e2e harness --set "<description>" [--start "<cmd>"] [--dir <path>]\n');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const harness = { description, recordedAt: new Date().toISOString() };
|
|
70
|
+
const st = args.indexOf('--start');
|
|
71
|
+
if (st >= 0 && args[st + 1]) harness.start = args[st + 1];
|
|
72
|
+
const di = args.indexOf('--dir');
|
|
73
|
+
if (di >= 0 && args[di + 1]) harness.dir = args[di + 1];
|
|
74
|
+
saveProjectConfig(process.cwd(), { e2e: { harness } }, { mergeKeys: ['adr', 'plan', 'e2e'] });
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
`Recorded harness in .forge/config.json (commit it). Future sessions will see it on forge e2e init.\n`,
|
|
77
|
+
);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
const h = loadHarness();
|
|
81
|
+
if (!h) {
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
'No harness recorded. After building one (with operator approval), record it:\n forge e2e harness --set "<what/where>" --start "<command>" [--dir <path>]\n',
|
|
84
|
+
);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`${harnessLines(h)}\n`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
let sessionId = null;
|
|
43
92
|
let force = false;
|
|
44
93
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -74,6 +123,8 @@ if (sub === 'init') {
|
|
|
74
123
|
process.stdout.write(
|
|
75
124
|
`Scaffolded ${file}\nAuthor the product-loop steps (produce → consume → assert domain side effects).\nSteps that would pass against a stubbed handler are invalid.\nRun with: forge e2e run\n`,
|
|
76
125
|
);
|
|
126
|
+
const harness = loadHarness();
|
|
127
|
+
if (harness) process.stdout.write(`\n${harnessLines(harness)}\n`);
|
|
77
128
|
process.exit(0);
|
|
78
129
|
}
|
|
79
130
|
|
|
@@ -133,7 +184,7 @@ if (sub === 'check') {
|
|
|
133
184
|
|
|
134
185
|
if (sub === 'status') {
|
|
135
186
|
if (!fs.existsSync(file)) {
|
|
136
|
-
process.stdout.write(JSON.stringify({ file, exists: false }, null, 2));
|
|
187
|
+
process.stdout.write(JSON.stringify({ file, exists: false, harness: loadHarness() }, null, 2));
|
|
137
188
|
process.stdout.write('\n');
|
|
138
189
|
process.exit(0);
|
|
139
190
|
}
|
|
@@ -153,6 +204,7 @@ if (sub === 'status') {
|
|
|
153
204
|
exists: true,
|
|
154
205
|
ok: valid.ok,
|
|
155
206
|
problems: valid.problems,
|
|
207
|
+
harness: loadHarness(),
|
|
156
208
|
results: results
|
|
157
209
|
? {
|
|
158
210
|
file: e2eResultsPath(dir),
|
|
@@ -87,11 +87,29 @@ a loop is complex exactly when the feature has many wiring seams, which is
|
|
|
87
87
|
when wiring silently breaks. Never skip the loop for complexity; control its
|
|
88
88
|
cost instead:
|
|
89
89
|
|
|
90
|
-
- **Ask before building
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
- **Ask before building complex harness infrastructure — and only then.**
|
|
91
|
+
The ask-line is infrastructure: a rig built from scratch, new
|
|
92
|
+
servers/containers/services, third-party accounts or sandboxes (e.g. real
|
|
93
|
+
payment checkout), device farms. That is a project, not a test — stop and
|
|
94
|
+
ask the user. **Everything below that line never needs permission**: if a
|
|
95
|
+
harness already exists (test server, isolated ports, scratch DB), author
|
|
96
|
+
the loop; writing fixtures, seed data, helper scripts, or extra steps
|
|
97
|
+
inside the existing rig is normal test work — just do it. Asking for
|
|
98
|
+
fixture-level work is over-asking; skipping the loop because the rig is
|
|
99
|
+
missing is under-testing — the ask is exactly the moment the operator can
|
|
100
|
+
say "build it once" and make every future loop on the project cheap.
|
|
101
|
+
- **A built harness is permanent project infrastructure — record it, reuse
|
|
102
|
+
it.** When the operator approves building one: build it as committed
|
|
103
|
+
project code (never session scratch), then record it:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
forge e2e harness --set "<what it is / where>" --start "<start command>" [--dir <path>]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This lands in `.forge/config.json` (committed), and every later session
|
|
110
|
+
sees it — `forge e2e init` and `forge e2e status` print the recorded
|
|
111
|
+
harness. Check `forge e2e harness` before proposing to build or asking the
|
|
112
|
+
operator; proposing a new rig while one is recorded is a REJECT.
|
|
95
113
|
- **One executed loop per capability, not per assertion.** Push edge cases
|
|
96
114
|
into unit tests; e2e proves the wiring exists.
|
|
97
115
|
- **Set `timeoutMs` from the step count up front.** A 10-step loop is never a
|