devlyn-cli 2.0.0 → 2.2.0
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/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/benchmark/auto-resolve/README.md +318 -2
- package/benchmark/auto-resolve/RUBRIC.md +6 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/NOTES.md +63 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/expected.json +60 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/setup.sh +17 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/spec.md +52 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/invalid.js +29 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/parallel.js +50 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/NOTES.md +70 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/expected.json +52 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/setup.sh +171 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/spec.md +51 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/NOTES.md +83 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/expected.json +74 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/setup.sh +251 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/spec.md +58 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/task.txt +13 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/verifiers/replay-malformed-body.js +64 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/NOTES.md +98 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/expected.json +46 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/setup.sh +336 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/spec.md +52 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/NOTES.md +26 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/expected.json +64 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/setup.sh +32 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/spec.md +58 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/exact-success.js +54 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/no-hardcoded-pricing.js +47 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/stock-error.js +45 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/expected.json +62 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/spec.md +62 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/error-order.js +55 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/priority-blocked.js +48 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/expected.json +56 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/spec.md +65 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/conflicting-duplicate.js +34 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/idempotent-close.js +41 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/expected.json +56 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/spec.md +71 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/priority-rollback.js +64 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/single-warehouse-fefo.js +66 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/NOTES.md +28 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/setup.sh +36 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/spec.md +65 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/catalog-source.js +57 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/exact-success.js +63 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/stock-error.js +34 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/NOTES.md +25 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/expected.json +68 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/setup.sh +17 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/spec.md +69 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/conflicting-duplicate.js +29 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/exact-payout.js +58 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/rules-source.js +56 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/NOTES.md +24 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/setup.sh +22 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/spec.md +62 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/exact-success.js +48 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/insufficient-balance.js +36 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/rules-source.js +55 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/NOTES.md +20 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/setup.sh +23 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/spec.md +66 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/task.txt +11 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/exact-success.js +44 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/rules-source.js +58 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/unavailable-inventory.js +35 -0
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +13 -1
- package/benchmark/auto-resolve/scripts/collect-swebench-predictions.py +98 -0
- package/benchmark/auto-resolve/scripts/fetch-swebench-instances.py +111 -0
- package/benchmark/auto-resolve/scripts/frozen-verify-gate.py +289 -0
- package/benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py +250 -0
- package/benchmark/auto-resolve/scripts/headroom-gate.py +147 -0
- package/benchmark/auto-resolve/scripts/judge.sh +82 -3
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py +244 -0
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py +118 -0
- package/benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py +192 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +234 -40
- package/benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh +511 -0
- package/benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh +162 -0
- package/benchmark/auto-resolve/scripts/run-headroom-candidate.sh +93 -0
- package/benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh +209 -0
- package/benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh +239 -0
- package/benchmark/auto-resolve/scripts/swebench-frozen-matrix.py +265 -0
- package/benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh +192 -0
- package/benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh +131 -0
- package/benchmark/auto-resolve/scripts/test-headroom-gate.sh +84 -0
- package/benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh +302 -0
- package/bin/devlyn.js +56 -10
- package/config/skills/_shared/archive_run.py +3 -0
- package/config/skills/_shared/codex-config.md +2 -2
- package/config/skills/_shared/codex-monitored.sh +72 -7
- package/config/skills/_shared/collect-codex-findings.py +125 -0
- package/config/skills/_shared/engine-preflight.md +1 -1
- package/config/skills/_shared/expected.schema.json +18 -0
- package/config/skills/_shared/spec-verify-check.py +312 -10
- package/config/skills/_shared/verify-merge-findings.py +327 -0
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:resolve/SKILL.md +62 -8
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +164 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +156 -4
- package/config/skills/devlyn:resolve/references/state-schema.md +10 -4
- package/package.json +1 -1
- package/scripts/lint-skills.sh +32 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# F15 setup — pre-stage a plausibly-correct items API with a latent
|
|
3
|
+
# load-time correctness issue. The implementation passes existing tests;
|
|
4
|
+
# the bug only surfaces when several writes are in flight together.
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
mkdir -p data
|
|
8
|
+
|
|
9
|
+
# Seed persisted state.
|
|
10
|
+
cat > data/items.json <<'JSON'
|
|
11
|
+
{
|
|
12
|
+
"items": [
|
|
13
|
+
{ "id": 1, "name": "alpha", "qty": 3 },
|
|
14
|
+
{ "id": 2, "name": "beta", "qty": 5 }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
JSON
|
|
18
|
+
|
|
19
|
+
# Pre-staged server implementation. Looks normal — uses fs/promises async
|
|
20
|
+
# I/O, validates input, returns sensible status codes. The latent issue is
|
|
21
|
+
# that POST /items reads-modifies-writes the JSON file with `await fs.read
|
|
22
|
+
# File` followed by `await fs.writeFile`, with no serialization. Two
|
|
23
|
+
# concurrent POSTs interleave during the await gap.
|
|
24
|
+
cat > server/index.js <<'JS'
|
|
25
|
+
const express = require('express');
|
|
26
|
+
const fs = require('fs/promises');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const app = express();
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
|
|
32
|
+
const DATA_PATH = path.join(__dirname, '..', 'data', 'items.json');
|
|
33
|
+
|
|
34
|
+
async function readStore() {
|
|
35
|
+
const text = await fs.readFile(DATA_PATH, 'utf8');
|
|
36
|
+
return JSON.parse(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writeStore(data) {
|
|
40
|
+
await fs.writeFile(DATA_PATH, JSON.stringify(data, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
app.get('/health', (_req, res) => {
|
|
44
|
+
res.json({ status: 'ok' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.get('/items', async (_req, res) => {
|
|
48
|
+
const data = await readStore();
|
|
49
|
+
res.json({ items: data.items });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.get('/items/:id', async (req, res) => {
|
|
53
|
+
const id = Number(req.params.id);
|
|
54
|
+
const data = await readStore();
|
|
55
|
+
const item = data.items.find((it) => it.id === id);
|
|
56
|
+
if (!item) {
|
|
57
|
+
res.status(404).json({ error: 'not_found', id });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.json({ item });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.post('/items', async (req, res) => {
|
|
64
|
+
const body = req.body || {};
|
|
65
|
+
if (typeof body.name !== 'string' || body.name.trim() === '') {
|
|
66
|
+
res.status(400).json({ error: 'invalid_body', field: 'name' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof body.qty !== 'number' || !Number.isInteger(body.qty) || body.qty <= 0) {
|
|
70
|
+
res.status(400).json({ error: 'invalid_body', field: 'qty' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const data = await readStore();
|
|
74
|
+
const newId = data.items.length + 1;
|
|
75
|
+
const newItem = { id: newId, name: body.name, qty: body.qty };
|
|
76
|
+
data.items.push(newItem);
|
|
77
|
+
await writeStore(data);
|
|
78
|
+
res.status(201).json({ item: newItem });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (require.main === module) {
|
|
82
|
+
const port = Number(process.env.PORT) || 3000;
|
|
83
|
+
app.listen(port, () => {
|
|
84
|
+
console.log(`bench-test-repo server listening on :${port}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { app };
|
|
89
|
+
JS
|
|
90
|
+
|
|
91
|
+
# Pre-staged tests covering existing happy paths. The arm must keep these
|
|
92
|
+
# passing AND add at least one test for the load-time fix.
|
|
93
|
+
cat > tests/server.test.js <<'JS'
|
|
94
|
+
const { test } = require('node:test');
|
|
95
|
+
const assert = require('node:assert');
|
|
96
|
+
const fs = require('node:fs');
|
|
97
|
+
const http = require('node:http');
|
|
98
|
+
const path = require('node:path');
|
|
99
|
+
const { app } = require('../server');
|
|
100
|
+
|
|
101
|
+
const DATA_PATH = path.join(__dirname, '..', 'data', 'items.json');
|
|
102
|
+
|
|
103
|
+
function startServer() {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const server = http.createServer(app);
|
|
106
|
+
server.listen(0, () => resolve(server));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function get(server, p) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const { port } = server.address();
|
|
113
|
+
http.get(`http://127.0.0.1:${port}${p}`, (res) => {
|
|
114
|
+
let body = '';
|
|
115
|
+
res.on('data', (c) => (body += c));
|
|
116
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(body) }));
|
|
117
|
+
}).on('error', reject);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function post(server, p, payload) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const { port } = server.address();
|
|
124
|
+
const data = JSON.stringify(payload);
|
|
125
|
+
const req = http.request(
|
|
126
|
+
{ host: '127.0.0.1', port, method: 'POST', path: p,
|
|
127
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
128
|
+
(res) => {
|
|
129
|
+
let body = '';
|
|
130
|
+
res.on('data', (c) => (body += c));
|
|
131
|
+
res.on('end', () => {
|
|
132
|
+
let parsed = null;
|
|
133
|
+
try { parsed = body ? JSON.parse(body) : null; } catch { parsed = body; }
|
|
134
|
+
resolve({ status: res.statusCode, body: parsed });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
req.on('error', reject);
|
|
139
|
+
req.write(data);
|
|
140
|
+
req.end();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function snapshotStore() { return fs.readFileSync(DATA_PATH); }
|
|
145
|
+
function restoreStore(snapshot) { fs.writeFileSync(DATA_PATH, snapshot); }
|
|
146
|
+
|
|
147
|
+
test('GET /health returns ok', async () => {
|
|
148
|
+
const s = await startServer();
|
|
149
|
+
try {
|
|
150
|
+
const r = await get(s, '/health');
|
|
151
|
+
assert.strictEqual(r.status, 200);
|
|
152
|
+
assert.deepStrictEqual(r.body, { status: 'ok' });
|
|
153
|
+
} finally { s.close(); }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('GET /items returns the persisted list', async () => {
|
|
157
|
+
const s = await startServer();
|
|
158
|
+
try {
|
|
159
|
+
const r = await get(s, '/items');
|
|
160
|
+
assert.strictEqual(r.status, 200);
|
|
161
|
+
assert.ok(Array.isArray(r.body.items));
|
|
162
|
+
assert.ok(r.body.items.length >= 2);
|
|
163
|
+
} finally { s.close(); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('GET /items/:id returns a single item', async () => {
|
|
167
|
+
const s = await startServer();
|
|
168
|
+
try {
|
|
169
|
+
const r = await get(s, '/items/1');
|
|
170
|
+
assert.strictEqual(r.status, 200);
|
|
171
|
+
assert.strictEqual(r.body.item.name, 'alpha');
|
|
172
|
+
} finally { s.close(); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('GET /items/:id returns 404 for missing', async () => {
|
|
176
|
+
const s = await startServer();
|
|
177
|
+
try {
|
|
178
|
+
const r = await get(s, '/items/99999');
|
|
179
|
+
assert.strictEqual(r.status, 404);
|
|
180
|
+
assert.strictEqual(r.body.error, 'not_found');
|
|
181
|
+
} finally { s.close(); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('POST /items appends a new item with a fresh id', async () => {
|
|
185
|
+
const snap = snapshotStore();
|
|
186
|
+
const s = await startServer();
|
|
187
|
+
try {
|
|
188
|
+
const before = await get(s, '/items');
|
|
189
|
+
const created = await post(s, '/items', { name: 'gamma', qty: 7 });
|
|
190
|
+
assert.strictEqual(created.status, 201);
|
|
191
|
+
assert.strictEqual(created.body.item.name, 'gamma');
|
|
192
|
+
const after = await get(s, '/items');
|
|
193
|
+
assert.strictEqual(after.body.items.length, before.body.items.length + 1);
|
|
194
|
+
} finally { s.close(); restoreStore(snap); }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('POST /items rejects invalid bodies with 400', async () => {
|
|
198
|
+
const snap = snapshotStore();
|
|
199
|
+
const s = await startServer();
|
|
200
|
+
try {
|
|
201
|
+
const r1 = await post(s, '/items', { name: 'noqty' });
|
|
202
|
+
assert.strictEqual(r1.status, 400);
|
|
203
|
+
const r2 = await post(s, '/items', { qty: 5 });
|
|
204
|
+
assert.strictEqual(r2.status, 400);
|
|
205
|
+
const r3 = await post(s, '/items', { name: 'bad', qty: -3 });
|
|
206
|
+
assert.strictEqual(r3.status, 400);
|
|
207
|
+
} finally { s.close(); restoreStore(snap); }
|
|
208
|
+
});
|
|
209
|
+
JS
|
|
210
|
+
|
|
211
|
+
# Verifier 1: concurrent POSTs must yield distinct ids and full length.
|
|
212
|
+
# This is the discriminator — current implementation FAILS this; a correct
|
|
213
|
+
# fix passes.
|
|
214
|
+
cat > data/_verify-concurrent.js <<'JS'
|
|
215
|
+
'use strict';
|
|
216
|
+
const http = require('http');
|
|
217
|
+
const fs = require('fs');
|
|
218
|
+
const { app } = require('../server');
|
|
219
|
+
|
|
220
|
+
const SNAP_PATH = 'data/items.json';
|
|
221
|
+
const SNAP = fs.readFileSync(SNAP_PATH);
|
|
222
|
+
|
|
223
|
+
const s = http.createServer(app).listen(0, async () => {
|
|
224
|
+
const { port } = s.address();
|
|
225
|
+
const post = (body) => new Promise((resolve) => {
|
|
226
|
+
const req = http.request(
|
|
227
|
+
{ host: '127.0.0.1', port, method: 'POST', path: '/items',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' } },
|
|
229
|
+
(r) => {
|
|
230
|
+
let b = ''; r.on('data', (c) => (b += c));
|
|
231
|
+
r.on('end', () => {
|
|
232
|
+
let d = null; try { d = JSON.parse(b); } catch {}
|
|
233
|
+
resolve({ status: r.statusCode, body: d });
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
req.on('error', () => resolve({ status: 0, body: null }));
|
|
238
|
+
req.write(JSON.stringify(body));
|
|
239
|
+
req.end();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const initial = JSON.parse(fs.readFileSync(SNAP_PATH, 'utf8'));
|
|
243
|
+
const startLen = initial.items.length;
|
|
244
|
+
|
|
245
|
+
const results = await Promise.all([
|
|
246
|
+
post({ name: 'gamma', qty: 1 }),
|
|
247
|
+
post({ name: 'delta', qty: 2 }),
|
|
248
|
+
post({ name: 'epsilon', qty: 3 }),
|
|
249
|
+
post({ name: 'zeta', qty: 4 }),
|
|
250
|
+
post({ name: 'eta', qty: 5 }),
|
|
251
|
+
]);
|
|
252
|
+
// Allow writes to settle.
|
|
253
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
254
|
+
|
|
255
|
+
const acked = results.filter((r) => r.status === 201);
|
|
256
|
+
const final = await new Promise((resolve) => {
|
|
257
|
+
http.get(`http://127.0.0.1:${port}/items`, (r) => {
|
|
258
|
+
let b = ''; r.on('data', (c) => (b += c));
|
|
259
|
+
r.on('end', () => {
|
|
260
|
+
let d = null; try { d = JSON.parse(b); } catch {}
|
|
261
|
+
resolve(d);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
const finalItems = (final && Array.isArray(final.items)) ? final.items : [];
|
|
266
|
+
const ids = finalItems.map((i) => i && i.id);
|
|
267
|
+
const uniqueIds = new Set(ids).size === ids.length;
|
|
268
|
+
const lengthOk = finalItems.length === startLen + acked.length;
|
|
269
|
+
|
|
270
|
+
// Each ack'd POST's claimed id must appear exactly once in final.
|
|
271
|
+
const ackIdsPresent = acked.every((r) =>
|
|
272
|
+
finalItems.some((it) => it && r.body && it.id === r.body.item.id)
|
|
273
|
+
);
|
|
274
|
+
const allAcked = acked.length === results.length;
|
|
275
|
+
|
|
276
|
+
const ok = allAcked && uniqueIds && lengthOk && ackIdsPresent;
|
|
277
|
+
console.log(JSON.stringify({
|
|
278
|
+
acked: acked.length, total: results.length, finalLen: finalItems.length,
|
|
279
|
+
startLen, uniqueIds, lengthOk, ackIdsPresent, ok,
|
|
280
|
+
}));
|
|
281
|
+
fs.writeFileSync(SNAP_PATH, SNAP); // restore for downstream verifiers
|
|
282
|
+
s.close();
|
|
283
|
+
process.exit(ok ? 0 : 1);
|
|
284
|
+
});
|
|
285
|
+
JS
|
|
286
|
+
|
|
287
|
+
# Verifier 2: single-POST regression — the fix must not break sequential use.
|
|
288
|
+
cat > data/_verify-single.js <<'JS'
|
|
289
|
+
'use strict';
|
|
290
|
+
const http = require('http');
|
|
291
|
+
const fs = require('fs');
|
|
292
|
+
const { app } = require('../server');
|
|
293
|
+
|
|
294
|
+
const SNAP_PATH = 'data/items.json';
|
|
295
|
+
const SNAP = fs.readFileSync(SNAP_PATH);
|
|
296
|
+
|
|
297
|
+
const s = http.createServer(app).listen(0, async () => {
|
|
298
|
+
const { port } = s.address();
|
|
299
|
+
const post = (body) => new Promise((resolve) => {
|
|
300
|
+
const req = http.request(
|
|
301
|
+
{ host: '127.0.0.1', port, method: 'POST', path: '/items',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' } },
|
|
303
|
+
(r) => {
|
|
304
|
+
let b = ''; r.on('data', (c) => (b += c));
|
|
305
|
+
r.on('end', () => {
|
|
306
|
+
let d = null; try { d = JSON.parse(b); } catch {}
|
|
307
|
+
resolve({ status: r.statusCode, body: d });
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
req.on('error', () => resolve({ status: 0, body: null }));
|
|
312
|
+
req.write(JSON.stringify(body));
|
|
313
|
+
req.end();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const r = await post({ name: 'solo_check', qty: 9 });
|
|
317
|
+
const get = await new Promise((resolve) => {
|
|
318
|
+
http.get(`http://127.0.0.1:${port}/items`, (rs) => {
|
|
319
|
+
let b = ''; rs.on('data', (c) => (b += c));
|
|
320
|
+
rs.on('end', () => {
|
|
321
|
+
let d = null; try { d = JSON.parse(b); } catch {}
|
|
322
|
+
resolve(d);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
const final = (get && Array.isArray(get.items)) ? get.items : [];
|
|
327
|
+
const present = final.some((it) => it && it.name === 'solo_check' && it.qty === 9);
|
|
328
|
+
const ok = r.status === 201 && r.body && r.body.item && r.body.item.name === 'solo_check' && present;
|
|
329
|
+
console.log(JSON.stringify({ status: r.status, present, ok }));
|
|
330
|
+
fs.writeFileSync(SNAP_PATH, SNAP);
|
|
331
|
+
s.close();
|
|
332
|
+
process.exit(ok ? 0 : 1);
|
|
333
|
+
});
|
|
334
|
+
JS
|
|
335
|
+
|
|
336
|
+
exit 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "F15-frozen-diff-race-review"
|
|
3
|
+
title: "Review and fix the items API for production load"
|
|
4
|
+
status: planned
|
|
5
|
+
complexity: high
|
|
6
|
+
depends-on: []
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F15 Review and fix the items API for production load
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
The `/items` API in `server/index.js` was implemented a few weeks ago by
|
|
14
|
+
another engineer. It has been running in staging without complaints —
|
|
15
|
+
existing tests pass and the happy paths work. The team is preparing for
|
|
16
|
+
a load test next quarter and wants the API reviewed for any correctness
|
|
17
|
+
issues that single-request testing would not surface.
|
|
18
|
+
|
|
19
|
+
The implementation persists state to `data/items.json` and exposes:
|
|
20
|
+
|
|
21
|
+
- `GET /health`
|
|
22
|
+
- `GET /items`
|
|
23
|
+
- `GET /items/:id`
|
|
24
|
+
- `POST /items`
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- [ ] Review `server/index.js` for any correctness issues a production load test would surface. Apply fixes you find. Common load-related issues include lost updates, duplicated identifiers, and inconsistent response/state pairs.
|
|
29
|
+
- [ ] Add at least one new test in `tests/server.test.js` covering the fix. Existing tests must still pass.
|
|
30
|
+
- [ ] If your fix introduces helpers, place them inside `server/index.js`. Do not split the server into multiple files.
|
|
31
|
+
- [ ] Behavioral contracts that callers depend on must remain unchanged: response shapes for all four endpoints, the `data/items.json` on-disk format, and the meaning of HTTP status codes.
|
|
32
|
+
|
|
33
|
+
## Constraints
|
|
34
|
+
|
|
35
|
+
- **No new npm dependencies.** Fix using Express + Node built-ins only.
|
|
36
|
+
- **No silent catches.** Errors surface with explicit status + body, not by returning a fake-success.
|
|
37
|
+
- **Touch only `server/index.js` and `tests/server.test.js`.** Do not modify `data/items.json` shape, `tests/cli.test.js`, or anything outside the server.
|
|
38
|
+
- **Lifecycle note.** The harness's DOCS phase flips this spec's frontmatter `status` after implementation completes — that is benchmark lifecycle bookkeeping, not a scope violation.
|
|
39
|
+
|
|
40
|
+
## Out of Scope
|
|
41
|
+
|
|
42
|
+
- Adding new endpoints, methods, or query parameters.
|
|
43
|
+
- Replacing the JSON-file store with a database.
|
|
44
|
+
- Reformatting or wholesale refactoring beyond what the fix requires.
|
|
45
|
+
- Performance tuning unrelated to the correctness issue (caching, pooling).
|
|
46
|
+
|
|
47
|
+
## Verification
|
|
48
|
+
|
|
49
|
+
- `node --test tests/server.test.js` exits 0.
|
|
50
|
+
- A small Node script that fires several `POST /items` requests close together must produce a state where every POST that returned `201` is reflected in `GET /items` exactly once, with distinct ids.
|
|
51
|
+
- Single-request behavior (one POST, then GET) is unchanged.
|
|
52
|
+
- `git diff --stat` shows only `server/index.js` and `tests/server.test.js` touched.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The `/items` API in `server/index.js` was implemented a few weeks ago by another engineer. It has been running in staging without complaints — existing tests pass and the happy paths work. The team is preparing for a load test next quarter and wants the API reviewed for any correctness issues a load test would surface that single-request testing would not.
|
|
2
|
+
|
|
3
|
+
The endpoints are: GET /health, GET /items, GET /items/:id, POST /items. State lives in `data/items.json`.
|
|
4
|
+
|
|
5
|
+
Review `server/index.js` and apply fixes for any issues you find. Common load-related issues include lost updates, duplicated identifiers, and inconsistent response/state pairs.
|
|
6
|
+
|
|
7
|
+
Add at least one new test in `tests/server.test.js` covering the fix. Existing tests must still pass. Behavioral contracts the callers depend on (response shapes, on-disk format, status code meanings) must remain unchanged.
|
|
8
|
+
|
|
9
|
+
No new npm dependencies. Only touch `server/index.js` and `tests/server.test.js`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# F16 CLI quote tax rules
|
|
2
|
+
|
|
3
|
+
## Failure mode
|
|
4
|
+
|
|
5
|
+
This fixture detects checkout implementations that pass visible happy-path
|
|
6
|
+
tests while missing product-grade invariants: duplicate-SKU aggregation,
|
|
7
|
+
combined stock validation, externalized pricing rules, exact JSON output,
|
|
8
|
+
and integer-cent tax/discount/shipping order.
|
|
9
|
+
|
|
10
|
+
## Pipeline phase target
|
|
11
|
+
|
|
12
|
+
PLAN must preserve calculation order and validation invariants. BUILD must
|
|
13
|
+
implement constrained arithmetic without adding dependencies. VERIFY should
|
|
14
|
+
catch edge cases that a shallow implementation or shallow tests miss.
|
|
15
|
+
|
|
16
|
+
## Why existing fixtures do not cover it
|
|
17
|
+
|
|
18
|
+
F1/F2 test CLI shape, but not business-rule arithmetic. F10/F11/F12 test
|
|
19
|
+
server behavior and persistence. F15 tests review behavior. None combine
|
|
20
|
+
hidden product math, exact machine output, and source-of-truth pricing.
|
|
21
|
+
|
|
22
|
+
## Retirement
|
|
23
|
+
|
|
24
|
+
Retire or replace this fixture if both bare and solo consistently score
|
|
25
|
+
above the headroom thresholds, or if future fixtures cover the same
|
|
26
|
+
calculation-order and hidden-verifier failure modes with better signal.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"verification_commands": [
|
|
3
|
+
{
|
|
4
|
+
"cmd": "node --test tests/cli.test.js",
|
|
5
|
+
"exit_code": 0,
|
|
6
|
+
"stdout_contains": [],
|
|
7
|
+
"stdout_not_contains": ["not ok "]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/exact-success.js\"",
|
|
11
|
+
"exit_code": 0,
|
|
12
|
+
"stdout_contains": ["\"ok\":true"],
|
|
13
|
+
"stdout_not_contains": [],
|
|
14
|
+
"contract_refs": [
|
|
15
|
+
"On success, write exactly one JSON object to stdout and no stderr. Keys: `subtotal_cents`, `discount_cents`, `tax_cents`, `shipping_cents`, `total_cents`, `items`.",
|
|
16
|
+
"Tax is computed on taxable line totals before any order-level discount.",
|
|
17
|
+
"`shipping_cents` is `0` when `subtotal_cents - discount_cents >= pricing.free_shipping_min_cents`; otherwise use `pricing.shipping_cents`."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/stock-error.js\"",
|
|
22
|
+
"exit_code": 0,
|
|
23
|
+
"stdout_contains": ["\"ok\":true"],
|
|
24
|
+
"stdout_not_contains": [],
|
|
25
|
+
"contract_refs": [
|
|
26
|
+
"Combined quantity over stock uses exact error shape `{ \"error\": \"invalid_stock\", \"sku\": string, \"available\": number, \"requested\": number }`.",
|
|
27
|
+
"The stock error object includes `sku`, `available`, and `requested`."
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/no-hardcoded-pricing.js\"",
|
|
32
|
+
"exit_code": 0,
|
|
33
|
+
"stdout_contains": ["\"ok\":true"],
|
|
34
|
+
"stdout_not_contains": [],
|
|
35
|
+
"contract_refs": [
|
|
36
|
+
"Pricing comes from `data/pricing.json`. Do not hardcode product prices, stock, tax codes, coupons, or tax rates in the command implementation."
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"forbidden_patterns": [
|
|
41
|
+
{
|
|
42
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
|
|
43
|
+
"description": "silent catch returning fallback in quote path",
|
|
44
|
+
"files": ["bin/cli.js"],
|
|
45
|
+
"severity": "disqualifier"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
|
|
49
|
+
"description": "empty catch block",
|
|
50
|
+
"files": ["bin/cli.js"],
|
|
51
|
+
"severity": "disqualifier"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"pattern": "/\\*\\s*eslint-disable",
|
|
55
|
+
"description": "eslint-disable without scoped justification",
|
|
56
|
+
"files": ["bin/cli.js"],
|
|
57
|
+
"severity": "disqualifier"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"required_files": ["bin/cli.js", "tests/cli.test.js", "data/pricing.json"],
|
|
61
|
+
"forbidden_files": [],
|
|
62
|
+
"max_deps_added": 0,
|
|
63
|
+
"spec_output_files": ["bin/cli.js", "tests/cli.test.js"]
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "F16-cli-quote-tax-rules",
|
|
3
|
+
"category": "high-risk",
|
|
4
|
+
"difficulty": "high",
|
|
5
|
+
"timeout_seconds": 1500,
|
|
6
|
+
"required_tools": ["node"],
|
|
7
|
+
"browser": false,
|
|
8
|
+
"deps_change_expected": false,
|
|
9
|
+
"intent": "Add a bench-cli quote command that reads an order JSON file, prices it from data/pricing.json, validates duplicate SKU quantities before quoting, and prints one exact JSON quote with cents-based discount, tax, shipping, and total fields."
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# F16 setup — seed quote pricing rules.
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
mkdir -p data
|
|
6
|
+
|
|
7
|
+
cat > data/pricing.json <<'JSON'
|
|
8
|
+
{
|
|
9
|
+
"products": {
|
|
10
|
+
"A": { "unit_cents": 1999, "stock": 3, "tax_code": "standard" },
|
|
11
|
+
"B": { "unit_cents": 250, "stock": 100, "tax_code": "exempt" },
|
|
12
|
+
"C": { "unit_cents": 5000, "stock": 2, "tax_code": "standard" }
|
|
13
|
+
},
|
|
14
|
+
"coupons": {
|
|
15
|
+
"SAVE10": { "percent": 10, "min_subtotal_cents": 2000 },
|
|
16
|
+
"BULK15": { "percent": 15, "min_subtotal_cents": 10000 }
|
|
17
|
+
},
|
|
18
|
+
"tax_rates": {
|
|
19
|
+
"CA": 0.0725,
|
|
20
|
+
"NY": 0.08875,
|
|
21
|
+
"OR": 0
|
|
22
|
+
},
|
|
23
|
+
"taxable_codes": {
|
|
24
|
+
"standard": true,
|
|
25
|
+
"exempt": false
|
|
26
|
+
},
|
|
27
|
+
"shipping_cents": 499,
|
|
28
|
+
"free_shipping_min_cents": 5000
|
|
29
|
+
}
|
|
30
|
+
JSON
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "F16-cli-quote-tax-rules"
|
|
3
|
+
title: "Quote command with tax rules"
|
|
4
|
+
status: planned
|
|
5
|
+
complexity: high
|
|
6
|
+
depends-on: []
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F16 Quote command with tax rules
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`bench-cli` currently has greeting and version commands only. The task:
|
|
14
|
+
add a `quote` command that reads an order JSON file, prices it from
|
|
15
|
+
`data/pricing.json`, validates duplicate SKU quantities before quoting,
|
|
16
|
+
and prints one exact JSON quote with cents-based discount, tax, shipping,
|
|
17
|
+
and total fields.
|
|
18
|
+
|
|
19
|
+
This is checkout math, so every calculation must use integer cents and
|
|
20
|
+
the output must be machine-readable.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- [ ] `bench-cli quote --input <path>` reads JSON shaped as `{ "state": string, "coupon": string | null, "items": [{ "sku": string, "qty": number }] }`.
|
|
25
|
+
- [ ] Pricing comes from `data/pricing.json`. Do not hardcode product prices, stock, tax codes, coupons, or tax rates in the command implementation.
|
|
26
|
+
- [ ] Combine duplicate SKUs before validating stock and before computing line totals. The output `items` array must contain one row per SKU in first-seen order.
|
|
27
|
+
- [ ] Validation happens before any quote is printed. Invalid JSON, missing `items`, unknown SKU, non-positive or non-integer `qty`, combined quantity over stock, unknown coupon, or unknown state exits `2` and writes exactly one JSON error object to stderr.
|
|
28
|
+
- [ ] Combined quantity over stock uses exact error shape `{ "error": "invalid_stock", "sku": string, "available": number, "requested": number }`.
|
|
29
|
+
- [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `subtotal_cents`, `discount_cents`, `tax_cents`, `shipping_cents`, `total_cents`, `items`.
|
|
30
|
+
- [ ] Each output item row has keys `sku`, `qty`, `line_cents`. `line_cents` is `unit_cents * combined_qty`.
|
|
31
|
+
- [ ] `discount_cents` is `Math.round(subtotal_cents * coupon.percent / 100)` when a coupon is present and the subtotal meets its `min_subtotal_cents`; otherwise `0`.
|
|
32
|
+
- [ ] Tax is computed on taxable line totals before any order-level discount. A product is taxable when its `tax_code` maps to `true` in `pricing.taxable_codes`. Tax rate is `pricing.tax_rates[state]`. Use `Math.round(taxable_subtotal_cents * rate)`.
|
|
33
|
+
- [ ] `shipping_cents` is `0` when `subtotal_cents - discount_cents >= pricing.free_shipping_min_cents`; otherwise use `pricing.shipping_cents`.
|
|
34
|
+
- [ ] `total_cents = subtotal_cents - discount_cents + tax_cents + shipping_cents`.
|
|
35
|
+
- [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover `quote`: one successful quote and one validation failure.
|
|
36
|
+
|
|
37
|
+
## Constraints
|
|
38
|
+
|
|
39
|
+
- **No new npm dependencies.**
|
|
40
|
+
- **No floating-money output.** All public amounts are integer cents.
|
|
41
|
+
- **No silent catches.** If parsing or file reading fails, emit a visible JSON error to stderr and exit `2`.
|
|
42
|
+
- **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
|
|
43
|
+
- **Lifecycle note.** The harness's DOCS phase flips this spec's frontmatter `status` after implementation completes — that is benchmark lifecycle bookkeeping, not a scope violation.
|
|
44
|
+
|
|
45
|
+
## Out of Scope
|
|
46
|
+
|
|
47
|
+
- Changing server routes or web UI.
|
|
48
|
+
- Inventory mutation or order persistence.
|
|
49
|
+
- Adding currencies, locales, or tax jurisdictions beyond `pricing.tax_rates`.
|
|
50
|
+
- Touching `server/`, `web/`, or `tests/server.test.js`.
|
|
51
|
+
|
|
52
|
+
## Verification
|
|
53
|
+
|
|
54
|
+
- `node --test tests/cli.test.js` exits 0.
|
|
55
|
+
- A quote with duplicate SKUs combines quantities before stock validation and emits exact integer-cent totals.
|
|
56
|
+
- A quote over combined stock exits `2`, prints one JSON error to stderr, and prints no stdout.
|
|
57
|
+
- The stock error object includes `sku`, `available`, and `requested`.
|
|
58
|
+
- `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched (the pricing seed comes from setup, not the arm).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Add a `quote` command to `bench-cli` so users can run `bench-cli quote --input <path>` with an order JSON file. The command should load prices, stock, coupons, tax rates, shipping, and taxable-code rules from `data/pricing.json`, then print one JSON quote with integer-cent fields for subtotal, discount, tax, shipping, total, and line items.
|
|
2
|
+
|
|
3
|
+
Duplicate SKUs in the order must be combined before stock validation and before totals are calculated. Invalid JSON, missing/invalid items, unknown SKU, too much combined quantity for stock, unknown coupon, or unknown state should exit `2`, print exactly one JSON error object to stderr, and print nothing to stdout.
|
|
4
|
+
|
|
5
|
+
On success, stdout must be exactly parseable JSON and stderr must be empty. Use integer cents only: no formatted currency strings and no floating-money fields. The coupon discount is rounded with `Math.round(subtotal_cents * coupon.percent / 100)`. Tax is rounded with `Math.round(taxable_subtotal_cents * state_rate)`, based on taxable line totals before the coupon. Shipping is free only when `subtotal_cents - discount_cents` reaches the configured free-shipping minimum.
|
|
6
|
+
|
|
7
|
+
Update `tests/cli.test.js` so existing tests still pass and add at least two quote tests: one success case and one validation failure. No new npm dependencies. Only touch `bin/cli.js` and `tests/cli.test.js`; `data/pricing.json` is seeded for you.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const orderPath = path.join(os.tmpdir(), `quote-order-${process.pid}.json`);
|
|
9
|
+
fs.writeFileSync(orderPath, JSON.stringify({
|
|
10
|
+
state: 'CA',
|
|
11
|
+
coupon: 'SAVE10',
|
|
12
|
+
items: [
|
|
13
|
+
{ sku: 'A', qty: 1 },
|
|
14
|
+
{ sku: 'B', qty: 3 },
|
|
15
|
+
{ sku: 'A', qty: 1 }
|
|
16
|
+
]
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const cli = path.join(process.env.BENCH_WORKDIR, 'bin', 'cli.js');
|
|
20
|
+
const result = spawnSync('node', [cli, 'quote', '--input', orderPath], {
|
|
21
|
+
cwd: process.env.BENCH_WORKDIR,
|
|
22
|
+
encoding: 'utf8'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let quote;
|
|
26
|
+
try {
|
|
27
|
+
quote = JSON.parse(result.stdout);
|
|
28
|
+
} catch {
|
|
29
|
+
quote = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const expected = {
|
|
33
|
+
subtotal_cents: 4748,
|
|
34
|
+
discount_cents: 475,
|
|
35
|
+
tax_cents: 290,
|
|
36
|
+
shipping_cents: 499,
|
|
37
|
+
total_cents: 5062,
|
|
38
|
+
items: [
|
|
39
|
+
{ sku: 'A', qty: 2, line_cents: 3998 },
|
|
40
|
+
{ sku: 'B', qty: 3, line_cents: 750 }
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ok = result.status === 0
|
|
45
|
+
&& result.stderr === ''
|
|
46
|
+
&& JSON.stringify(quote) === JSON.stringify(expected);
|
|
47
|
+
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
ok,
|
|
50
|
+
status: result.status,
|
|
51
|
+
stderr: result.stderr,
|
|
52
|
+
quote
|
|
53
|
+
}));
|
|
54
|
+
process.exit(ok ? 0 : 1);
|