* fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow The thread workflow's CLOSE and RESUME branches called frontmatter.set with the pre-1.6 fully-positional shape (frontmatter.set <file> <field> <value>). Since 1.6 the dispatcher (gsd-tools.cjs) parses the file positionally and reads field/value from the named flags --field/--value via parseNamedArgs; the positional form leaves field/value undefined, cmdFrontmatterSet errors 'file, field, and value required', and the status/updated writes are silently skipped. Closing a thread never marked it status: resolved and resuming never marked it status: in_progress. Switch all four sites (CLOSE status+updated, RESUME status+updated) to the 1.6 hybrid form that verify-work.md already uses: frontmatter.set <file> --field <field> --value <value> Add a regression test with three guards: (1) behavioral — the named-flag form writes the field while the positional form errors with the documented message and does not mutate the file; (2) workflow parity — no workflow under gsd-core/workflows/ emits the positional form, so a future edit that reintroduces it anywhere fails CI; (3) thread-specific — CLOSE writes status: resolved and RESUME writes status: in_progress via the named flags. * docs(#1778): add changeset fragment for thread workflow frontmatter fix * docs(#1778): fix unclosed inline-code backtick in changeset fragment * fix(#1778): move regression into owning test + regen baselines lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the #1778 regression (behavioral named-vs-positional + workflow-parity scan + thread CLOSE/RESUME assertions) into tests/frontmatter-cli.test.cjs, the canonical home for frontmatter CLI regressions, and delete the standalone file. frontmatter-cli.test.cjs already carries the allow-test-rule exemption for workflow .md content tests. gsd-core/workflows/thread.md ships to every runtime and is size-tracked, so recapture the 16 golden-install-parity fixtures (thread.md hash) and the per-file workflow size baseline (thread.md 12400 -> 12464) via UPDATE_GOLDEN=1 and npm run size:baseline.
This commit is contained in:
@@ -18,7 +18,7 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
const { runGsdTools, parseFrontmatter } = require('./helpers.cjs');
|
||||
|
||||
// Track temp files for cleanup
|
||||
let tempFiles = [];
|
||||
@@ -441,3 +441,88 @@ describe('Bug #1660: frontmatter set of an object-list field fails closed instea
|
||||
assert.ok(!parsed.error, 'an idempotent scalar-array set must not produce an error');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #1778: thread workflow must use the 1.6 named-flag frontmatter.set form ─
|
||||
//
|
||||
// The thread workflow's CLOSE and RESUME branches previously invoked the
|
||||
// pre-1.6 positional shape (frontmatter.set <file> <field> <value>). Since 1.6
|
||||
// the dispatcher (gsd-tools.cjs) reads field/value from the named --field/
|
||||
// --value flags via parseNamedArgs; the positional form leaves field/value
|
||||
// undefined, cmdFrontmatterSet errors `file, field, and value required`, and
|
||||
// the status/updated writes are silently skipped — so closing a thread never
|
||||
// marked it status: resolved and resuming never marked it status: in_progress.
|
||||
describe('#1778: thread workflow uses the 1.6 named-flag frontmatter.set form', () => {
|
||||
test('behavioral: named-flag form writes the field; positional form errors and does not mutate', () => {
|
||||
// 1.6 named-flag form — must succeed and write status: resolved.
|
||||
const goodFile = writeTempFile('---\nstatus: open\nupdated: "2025-01-01"\n---\n\n# thread body\n');
|
||||
const good = runGsdTools(['frontmatter', 'set', goodFile, '--field', 'status', '--value', 'resolved']);
|
||||
assert.ok(good.success, `named-flag form must succeed; stderr: ${good.error}`);
|
||||
assert.strictEqual(
|
||||
parseFrontmatter(fs.readFileSync(goodFile, 'utf-8')).status,
|
||||
'resolved',
|
||||
'named-flag form must write status: resolved into the file',
|
||||
);
|
||||
|
||||
// Pre-1.6 positional form — must fail with the documented message and NOT mutate.
|
||||
const badFile = writeTempFile('---\nstatus: open\nupdated: "2025-01-01"\n---\n\n# thread body\n');
|
||||
const bad = runGsdTools(['frontmatter', 'set', badFile, 'status', 'resolved']);
|
||||
assert.ok(!bad.success, 'positional form must fail (it is the bug being guarded against)');
|
||||
assert.ok(
|
||||
(bad.error + bad.output).includes('file, field, and value required'),
|
||||
`positional form must error with the documented message; got:\n${bad.error}${bad.output}`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
parseFrontmatter(fs.readFileSync(badFile, 'utf-8')).status,
|
||||
'open',
|
||||
'positional form must NOT mutate the file (the silent-failure bug)',
|
||||
);
|
||||
});
|
||||
|
||||
test('workflow parity: no gsd-core/workflows/*.md emits the positional frontmatter.set form', () => {
|
||||
const workflowsDir = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
const files = fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
|
||||
assert.ok(files.length > 0, 'expected at least one workflow under gsd-core/workflows/');
|
||||
|
||||
const offenders = [];
|
||||
for (const name of files) {
|
||||
const full = path.join(workflowsDir, name);
|
||||
const lines = fs.readFileSync(full, 'utf-8').split(/\r?\n/);
|
||||
lines.forEach((line, i) => {
|
||||
// Match any frontmatter.set invocation (dot or space form, with or
|
||||
// without the `gsd_run query` prefix). The 1.6 contract requires
|
||||
// --field AND --value on every set call; a set line missing --field
|
||||
// is the pre-1.6 positional form (#1778).
|
||||
if (!/frontmatter[.\s]+set\b/.test(line)) return;
|
||||
if (!/--field\b/.test(line) || !/--value\b/.test(line)) {
|
||||
offenders.push(`${name}:${i + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
offenders,
|
||||
[],
|
||||
`These workflow frontmatter.set invocations are missing the 1.6 --field/--value named flags (the #1778 positional-form bug):\n ${offenders.join('\n ')}\n\nUse: gsd_run query frontmatter.set <file> --field <field> --value <value>`,
|
||||
);
|
||||
});
|
||||
|
||||
test('thread workflow CLOSE writes status: resolved and RESUME writes status: in_progress via named flags', () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'workflows', 'thread.md'), 'utf-8');
|
||||
|
||||
// CLOSE mode: status resolved + updated, both via named flags.
|
||||
assert.ok(
|
||||
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+status\s+--value\s+resolved\b/.test(src),
|
||||
'CLOSE mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field status --value resolved',
|
||||
);
|
||||
assert.ok(
|
||||
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+updated\s+--value\s+YYYY-MM-DD\b/.test(src),
|
||||
'CLOSE mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field updated --value YYYY-MM-DD',
|
||||
);
|
||||
|
||||
// RESUME mode: status in_progress + updated, both via named flags.
|
||||
assert.ok(
|
||||
/frontmatter\.set\s+\S*\.planning\/threads\/\{SLUG\}\.md\s+--field\s+status\s+--value\s+in_progress\b/.test(src),
|
||||
'RESUME mode must invoke: frontmatter.set .planning/threads/{SLUG}.md --field status --value in_progress',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user