@bongos/core 1.19.879 → 1.19.881
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/.bongos-core.json +39 -34
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +0 -4
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +220 -220
- package/docs/copy-registry.json +263 -263
- package/docs/module-api-changelog.md +4 -0
- package/docs/project-context.template.md +2 -2
- package/modules/hall-ui/public/dom-utils.js +18 -2
- package/modules/hall-ui/public/goals-render.js +8 -3
- package/modules/hall-ui/public/primer.js +0 -1
- package/modules/hall-ui/public/roadmap.css +0 -11
- package/modules/hall-ui/public/roadmap.js +7 -15
- package/modules/hall-ui/public/task.js +6 -2
- package/modules/hall-ui/public/work.js +4 -2
- package/modules/lifecycle/db-versions.js +17 -13
- package/modules/lifecycle/routes/versions.js +1 -11
- package/modules/lifecycle/version-autoclose.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/init.js +0 -3
- package/scripts/gds/provision-repo.js +5 -4
- package/src/module-api.js +1 -1
- package/tests/hall_record_world.mjs +5 -0
- package/tests/init.mjs +15 -12
- package/tests/kind_axis_unset.mjs +195 -0
- package/tests/provision.mjs +8 -3
- package/tests/version_first_goal.mjs +4 -2
- package/tests/versions_route.mjs +37 -12
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// tests/kind_axis_unset.mjs — the KIND axis never reads its column default to a
|
|
2
|
+
// person (task 1004038, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// The twin of the role axis task 1003936 fixed. `tasks.kind` defaults to
|
|
5
|
+
// 'unclassified'; docs/voice.md V2 says a value that exists because a column
|
|
6
|
+
// needed a default is not a word a person should read. Before this, the word
|
|
7
|
+
// reached a reader on four surfaces: the Goals page row tag, the Tasks board row
|
|
8
|
+
// tag, the backlog history row, and every Kind filter's option list.
|
|
9
|
+
//
|
|
10
|
+
// The line this holds — the same one 1003936 drew: the STORED value and the
|
|
11
|
+
// FILTER VALUE stay 'unclassified'. Only what is READ changes. A test that let
|
|
12
|
+
// the value change would be pinning the wrong half.
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
23
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
24
|
+
|
|
25
|
+
// The real dom-utils.js, booted the way the pages boot it, so these are the
|
|
26
|
+
// functions that ship and not a restatement of them.
|
|
27
|
+
function loadOTB() {
|
|
28
|
+
const mkEl = () => ({
|
|
29
|
+
value: '', textContent: '', length: 0, children: [],
|
|
30
|
+
appendChild(c) { this.children.push(c); this.length = this.children.length; },
|
|
31
|
+
setAttribute() {}, addEventListener() {},
|
|
32
|
+
});
|
|
33
|
+
const sandbox = {
|
|
34
|
+
window: { __BRANDING__: {}, __MODULES__: { enabled: {} }, addEventListener() {} },
|
|
35
|
+
document: { createElement: mkEl, getElementById: () => null, querySelector: () => null,
|
|
36
|
+
querySelectorAll: () => [], addEventListener() {} },
|
|
37
|
+
console,
|
|
38
|
+
};
|
|
39
|
+
sandbox.globalThis = sandbox;
|
|
40
|
+
vm.createContext(sandbox);
|
|
41
|
+
vm.runInContext(read('dom-utils.js'), sandbox, { filename: 'dom-utils.js' });
|
|
42
|
+
return { OTB: sandbox.window.OTB };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('the unset kind is asked about, not spelled, and the stored value is untouched', () => {
|
|
46
|
+
const { OTB } = loadOTB();
|
|
47
|
+
assert.equal(OTB.KIND_UNSET, 'unclassified',
|
|
48
|
+
'the STORED value is still the column default — this fix changes reading, not writing');
|
|
49
|
+
assert.equal(OTB.kindIsUnset('unclassified'), true);
|
|
50
|
+
assert.equal(OTB.kindIsUnset(''), true, 'a missing kind is unset too');
|
|
51
|
+
assert.equal(OTB.kindIsUnset(null), true);
|
|
52
|
+
assert.equal(OTB.kindIsUnset(undefined), true);
|
|
53
|
+
assert.equal(OTB.kindIsUnset('bug'), false, 'a real kind is never unset');
|
|
54
|
+
assert.equal(OTB.kindIsUnset('verify'), false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a kind is named for a reader, and the unset one is named a phrase a person would say', () => {
|
|
58
|
+
const { OTB } = loadOTB();
|
|
59
|
+
assert.equal(OTB.kindLabel('unclassified'), 'No kind yet',
|
|
60
|
+
"the column's word never reaches a reader");
|
|
61
|
+
assert.doesNotMatch(OTB.kindLabel('unclassified'), /unclassified/i);
|
|
62
|
+
// The labels task 1003453 shipped are unchanged — this fix adds a case, it
|
|
63
|
+
// does not restyle the vocabulary.
|
|
64
|
+
assert.equal(OTB.kindLabel('bug'), 'Bug');
|
|
65
|
+
assert.equal(OTB.kindLabel('blocker-resolution'), 'Blocker resolution');
|
|
66
|
+
assert.equal(OTB.kindLabel('learning-capture'), 'Learning capture');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('EVERY Kind picker names the unset bucket, filters included — and keeps its value', () => {
|
|
70
|
+
const { OTB } = loadOTB();
|
|
71
|
+
const sel = { value: '', length: 1, options: [],
|
|
72
|
+
appendChild(o) { this.options.push(o); this.length = this.options.length + 1; } };
|
|
73
|
+
// A FILTER: `label` is identity, as work.js and goals-page.js pass it. Before
|
|
74
|
+
// this fix that identity function is exactly what leaked the column's word.
|
|
75
|
+
OTB.fillKindFilter(sel, ['bug', 'verify', 'unclassified'], { keepFirst: 1 });
|
|
76
|
+
const unset = sel.options.find((o) => o.value === 'unclassified');
|
|
77
|
+
assert.ok(unset, 'the unset bucket is still SELECTABLE — a filter you cannot apply is a lost lens');
|
|
78
|
+
assert.equal(unset.value, 'unclassified', 'and it still filters on the stored value');
|
|
79
|
+
assert.equal(unset.textContent, 'No kind yet', 'but it READS as a phrase');
|
|
80
|
+
// The real kinds still honour the caller's label choice.
|
|
81
|
+
assert.equal(sel.options.find((o) => o.value === 'bug').textContent, 'bug',
|
|
82
|
+
"a filter's raw-value labelling for real kinds is unchanged");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- the render sites ------------------------------------------------------
|
|
86
|
+
// Source pins, the idiom tests/hall_board_world.mjs uses for the role twin: a
|
|
87
|
+
// row tag is built in a template literal that no unit test can reach without
|
|
88
|
+
// booting the whole page.
|
|
89
|
+
|
|
90
|
+
const goalsRender = read('goals-render.js');
|
|
91
|
+
const workJs = read('work.js');
|
|
92
|
+
const taskJs = read('task.js');
|
|
93
|
+
|
|
94
|
+
// Every `t.kind` USE in the two files is accounted for — the structural form,
|
|
95
|
+
// not a pattern match. task 1004038's first round asserted the literal
|
|
96
|
+
// `t.kind || 'unclassified'` was gone, which was true and still missed
|
|
97
|
+
// taskRowsDone()'s `t.kind || '—'`: a different spelling of the same bug, in a
|
|
98
|
+
// file the diff was already editing. `||` cannot guard this axis AT ALL, because
|
|
99
|
+
// the default is the truthy string 'unclassified' — so the test has to enumerate
|
|
100
|
+
// every use and demand each one either guards or is a declared non-render.
|
|
101
|
+
// Each scanned file split ONCE at module load — the scan below reads these, so
|
|
102
|
+
// adding a case costs no re-read (the per-test split was the round-2 note).
|
|
103
|
+
const scanned = (src) => {
|
|
104
|
+
const all = src.split('\n');
|
|
105
|
+
return { raw: src, all, hits: all.map((l, i) => [l, i]).filter(([l]) => l.includes('t.kind')) };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const USES = {
|
|
109
|
+
'goals-render.js': {
|
|
110
|
+
src: scanned(goalsRender),
|
|
111
|
+
// A use that never reaches a reader, each with the reason it is exempt.
|
|
112
|
+
nonRender: {
|
|
113
|
+
"case 'kind': return String(t.kind || '').toLowerCase();": 'a SORT key — compared, never displayed',
|
|
114
|
+
"if (kFilter && (t.kind || window.OTB.KIND_UNSET) !== kFilter) return false;": 'the filter PREDICATE — must use the stored value',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
'work.js': {
|
|
118
|
+
src: scanned(workJs),
|
|
119
|
+
nonRender: {
|
|
120
|
+
'if (f.kind && t.kind !== f.kind) return false;': 'the board filter PREDICATE — stored value again',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
// Round 2 found the RECORD page reading the word too, via `if (t.kind)` — which
|
|
124
|
+
// is not a guard, because the default is truthy. Enumerating the file here is
|
|
125
|
+
// what stops the next one being found by a grader instead of by this test.
|
|
126
|
+
'task.js': {
|
|
127
|
+
src: scanned(taskJs),
|
|
128
|
+
nonRender: {
|
|
129
|
+
"if (t.kind === 'verify' && !window.confirm(": 'a BRANCH on the value — gates a confirm prompt, renders nothing',
|
|
130
|
+
"const body = t.kind === 'verify' ? { verified: true } : {};": 'a REQUEST body — sent to the route, never displayed',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
test('every t.kind use either goes through kindIsUnset or is a declared non-render', () => {
|
|
136
|
+
for (const [name, { src, nonRender }] of Object.entries(USES)) {
|
|
137
|
+
const { all, hits } = src;
|
|
138
|
+
assert.ok(hits.length, `${name} has t.kind uses to check`);
|
|
139
|
+
for (const [raw, i] of hits) {
|
|
140
|
+
const line = raw.trim();
|
|
141
|
+
if (line.startsWith('//') || line.startsWith('*')) continue; // prose about the axis
|
|
142
|
+
if (Object.keys(nonRender).some((k) => line.includes(k.trim()))) continue;
|
|
143
|
+
// The guard may be a multi-line ternary whose `?`/`:` arms sit on their own
|
|
144
|
+
// lines (goals-render.js writes it that way), so the window is the
|
|
145
|
+
// statement, not the line — two lines back is enough for every form here.
|
|
146
|
+
const stmt = all.slice(Math.max(0, i - 2), i + 1).join('\n');
|
|
147
|
+
assert.match(stmt, /kindIsUnset/,
|
|
148
|
+
`${name}: this t.kind use neither guards nor is declared a non-render — `
|
|
149
|
+
+ `if it renders, the column's word reaches a reader:\n ${line}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('the column default is TRUTHY, so `||` can never be the guard', () => {
|
|
155
|
+
// The fact that makes the finding above a bug rather than a style note, and
|
|
156
|
+
// the reason no render site may fall back with `||`. Pinned so a future reader
|
|
157
|
+
// does not "simplify" a kindIsUnset() call back into a falsy check.
|
|
158
|
+
const { OTB } = loadOTB();
|
|
159
|
+
assert.ok(OTB.KIND_UNSET, 'the default is a non-empty string');
|
|
160
|
+
assert.equal(!!(OTB.KIND_UNSET || '—'), true, '`t.kind || fallback` keeps the default, never the fallback');
|
|
161
|
+
assert.equal(OTB.KIND_UNSET || '—', 'unclassified', 'which is exactly the word that must not be read');
|
|
162
|
+
for (const [name, { src }] of Object.entries(USES)) {
|
|
163
|
+
assert.doesNotMatch(src.raw, /escapeHtml\(t\.kind \|\|/,
|
|
164
|
+
`${name} never renders a \`||\` fallback for the kind axis — it cannot fire`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('a kind nobody set renders NO tag, exactly as an unset role does', () => {
|
|
169
|
+
for (const [name, src] of [['goals-render.js', goalsRender], ['work.js', workJs]]) {
|
|
170
|
+
assert.match(src, /window\.OTB\.kindIsUnset\(t\.kind\)/,
|
|
171
|
+
`${name} asks the vocabulary whether the kind is set`);
|
|
172
|
+
assert.match(src, /kindTag/, `${name} renders through the guarded tag`);
|
|
173
|
+
}
|
|
174
|
+
// The backlog history row is the third surface and the easiest to miss — it
|
|
175
|
+
// rendered `t.kind || ''`, which is empty-safe but says 'unclassified' out loud.
|
|
176
|
+
const history = workJs.slice(workJs.indexOf('class="history__row"'));
|
|
177
|
+
assert.match(history, /kindIsUnset\(t\.kind\) \? '' : escapeHtml\(t\.kind\)/,
|
|
178
|
+
'the backlog history row is guarded too');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a TABLE cell keeps its placeholder — 'no tag' is not 'no cell'", () => {
|
|
182
|
+
// taskRowsDone()'s Completed ledger is a table: a vanished <td> would shift
|
|
183
|
+
// every following column. It guards like the rest but falls back to the
|
|
184
|
+
// em-dash the ledger already used for an empty cell.
|
|
185
|
+
const done = goalsRender.slice(goalsRender.indexOf('function taskRowsDone'));
|
|
186
|
+
assert.match(done.slice(0, done.indexOf('function ', 10)), /kindIsUnset\(t\.kind\) \? '—' : escapeHtml\(t\.kind\)/,
|
|
187
|
+
'the ledger cell is guarded and still renders a placeholder');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('the filter PREDICATE still compares against the stored value', () => {
|
|
191
|
+
// If this ever reads the label instead, every Kind filter silently matches
|
|
192
|
+
// nothing and the board looks empty rather than broken.
|
|
193
|
+
assert.match(goalsRender, /t\.kind \|\| window\.OTB\.KIND_UNSET/,
|
|
194
|
+
'the goals-page predicate defaults to the stored value, through the constant');
|
|
195
|
+
});
|
package/tests/provision.mjs
CHANGED
|
@@ -59,16 +59,21 @@ t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
|
|
|
59
59
|
assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, track
|
|
62
|
+
t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, no track, one criterion — the fix for the empty-versions bug (task 2119)', () => {
|
|
63
63
|
const cmd = P.seedFirstVersionCmd(coTenant);
|
|
64
64
|
// targets the instance's OWN db, matching createdb/migrate
|
|
65
65
|
assert.match(cmd, /psql .*-d mercury /, 'seeds the instance db');
|
|
66
66
|
assert.match(cmd, /ON_ERROR_STOP=1/, 'aborts on any error (atomic, no half-seed)');
|
|
67
67
|
// the version the greenfield init spec defines — ONE source of truth
|
|
68
68
|
const v = P.instanceInitSpec(coTenant).version;
|
|
69
|
-
assert.match(cmd, /INSERT INTO versions \(id, name,
|
|
69
|
+
assert.match(cmd, /INSERT INTO versions \(id, name, status, done_when\)/);
|
|
70
70
|
assert.match(cmd, new RegExp(`\\$v\\$${reEsc(v.id)}\\$v\\$`), 'seeds the spec version id (V1)');
|
|
71
|
-
|
|
71
|
+
// task 1003665 retired the two-track model. The seed names no track: it runs on
|
|
72
|
+
// a freshly-MIGRATED database, so core_205's DEFAULT 'product' fills the still-
|
|
73
|
+
// NOT NULL column. If that default is ever dropped, THIS seed 500s first and a
|
|
74
|
+
// new instance cannot stand up — which is why the assertion is stated that way.
|
|
75
|
+
assert.doesNotMatch(cmd, /\btrack\b/, 'the seed names no track column');
|
|
76
|
+
assert.doesNotMatch(cmd, /\$v\$product\$v\$/, 'nor smuggles its value in positionally');
|
|
72
77
|
assert.match(cmd, /\$v\$building\$v\$/, 'status building, mirroring seedViaApi');
|
|
73
78
|
assert.ok(cmd.includes(v.done_when), 'carries the spec done_when verbatim (incl. the → arrows)');
|
|
74
79
|
// the IDEMPOTENT guard — only seeds when there is NO version yet, so it never disturbs an existing project
|
|
@@ -57,7 +57,9 @@ function fakeDb({ goalRow = { id: 77, title: 'Foundations' } } = {}) {
|
|
|
57
57
|
query: async (sql, params) => {
|
|
58
58
|
log.push({ sql, params });
|
|
59
59
|
if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(sql)) return { rows: [] };
|
|
60
|
-
|
|
60
|
+
// params are positional: (id, name, status, scope_doc_path, done_when).
|
|
61
|
+
// `track` sat at index 3 until task 1003665 removed it from the INSERT.
|
|
62
|
+
if (/INSERT INTO versions/i.test(sql)) return { rows: [{ id: params[0], name: params[1], status: params[2] }] };
|
|
61
63
|
if (/INSERT INTO goals/i.test(sql)) return { rows: [goalRow] };
|
|
62
64
|
if (/INSERT INTO done_when_criteria/i.test(sql)) {
|
|
63
65
|
return { rows: [{ id: log.length, version_id: params[0], criterion_id: params[1], criterion_md: params[2], sort_order: params[3], goal_id: params[4], satisfied: false }] };
|
|
@@ -83,7 +85,7 @@ const CRITERIA = [
|
|
|
83
85
|
console.log('createVersion — the version, its first goal, and its criteria:');
|
|
84
86
|
|
|
85
87
|
const out = await createVersion({
|
|
86
|
-
id: 'V1', name: 'Foundations', status: 'building',
|
|
88
|
+
id: 'V1', name: 'Foundations', status: 'building',
|
|
87
89
|
doneWhen: 'The loop closes.', criteria: CRITERIA, createdBy: 7,
|
|
88
90
|
}, { pool: db.pool });
|
|
89
91
|
|
package/tests/versions_route.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { strict as assert } from 'node:assert';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import fs from 'node:fs';
|
|
13
14
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const { validate } = require('../src/bongos/routes/_helpers.js');
|
|
@@ -23,12 +24,14 @@ function test(name, fn) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// The EXACT schema declared in versions.js → POST /versions validateOrRespond().
|
|
26
|
-
// Keep in sync if the route's schema changes
|
|
27
|
+
// Keep in sync if the route's schema changes — and note that "keep in sync" is a
|
|
28
|
+
// promise a copy cannot keep on its own, so the track case below reads the
|
|
29
|
+
// ROUTE SOURCE rather than this object. A copy that still declared `track` would
|
|
30
|
+
// happily prove the old contract forever.
|
|
27
31
|
const VERSIONS_POST_SCHEMA = {
|
|
28
32
|
id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
|
|
29
33
|
name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
|
|
30
34
|
status: { type: 'string', maxLength: 32 },
|
|
31
|
-
track: { type: 'string', maxLength: 16 },
|
|
32
35
|
done_when: { type: 'string', maxLength: 8000 },
|
|
33
36
|
scope_doc_path: { type: 'string', maxLength: 256 },
|
|
34
37
|
criteria: { type: 'array', maxItems: 100 },
|
|
@@ -75,18 +78,40 @@ test('INIT: unknown extra fields pass through (forward-compat)', () => {
|
|
|
75
78
|
assert.equal(validate({ id: 'V1', name: 'x', future_field: 42 }, VERSIONS_POST_SCHEMA), null);
|
|
76
79
|
});
|
|
77
80
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// The two-track (product/internal) model was retired by task 1002006 and its code
|
|
82
|
+
// unwound by task 1003665: role/discipline is the sole work-type axis. These read
|
|
83
|
+
// the ROUTE'S OWN SOURCE, not the copied schema above — the point is that the
|
|
84
|
+
// concept is gone from the file that enforces it.
|
|
85
|
+
//
|
|
86
|
+
// What made the removal safe, and what would make it unsafe again: versions.track
|
|
87
|
+
// is still NOT NULL, so an INSERT omitting it is legal ONLY because core_205 gave
|
|
88
|
+
// the column DEFAULT 'product'. Drop that default and every create 500s. The
|
|
89
|
+
// comments this task deleted asserted the opposite ("no DB default") and were
|
|
90
|
+
// stale from core_205 onward — which is precisely why five insert paths went on
|
|
91
|
+
// supplying a value nothing read.
|
|
92
|
+
const routeSrc = fs.readFileSync(
|
|
93
|
+
new URL('../modules/lifecycle/routes/versions.js', import.meta.url), 'utf8');
|
|
94
|
+
|
|
95
|
+
test('the route no longer knows what a track is', () => {
|
|
96
|
+
for (const gone of ['VALID_VERSION_TRACKS', 'bad_track']) {
|
|
97
|
+
assert.ok(!routeSrc.includes(gone), `${gone} is gone from POST /versions`);
|
|
98
|
+
}
|
|
99
|
+
assert.ok(!/\btrack\b/.test(routeSrc.replace(/tracked|tracking/g, '')),
|
|
100
|
+
'no track identifier survives in the versions route');
|
|
84
101
|
});
|
|
85
102
|
|
|
86
|
-
test('
|
|
87
|
-
|
|
88
|
-
assert.
|
|
89
|
-
assert.
|
|
103
|
+
test('a version is created WITHOUT a track, and that is the whole contract', () => {
|
|
104
|
+
// The create shape `bongos init` and provision-repo.js now send.
|
|
105
|
+
assert.equal(validate({ id: 'V1', name: 'x' }, VERSIONS_POST_SCHEMA), null);
|
|
106
|
+
assert.equal(validate({ id: 'GDS-V5', name: 'x', status: 'planning' }, VERSIONS_POST_SCHEMA), null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the INSERT does not name the column either', () => {
|
|
110
|
+
const dbSrc = fs.readFileSync(
|
|
111
|
+
new URL('../modules/lifecycle/db-versions.js', import.meta.url), 'utf8');
|
|
112
|
+
const insert = dbSrc.slice(dbSrc.indexOf('INSERT INTO versions'));
|
|
113
|
+
assert.ok(!/^[^`]*\btrack\b/.test(insert.slice(0, insert.indexOf('`'))),
|
|
114
|
+
'INSERT INTO versions names no track column — the DB default supplies it');
|
|
90
115
|
});
|
|
91
116
|
|
|
92
117
|
// ---- BAD shapes must return a structured 400, never throw / 500 ----
|