Skip to content

Commit 03b2979

Browse files
committed
test(migrate): make "a migration owns the prose" enforceable
Follow-up to the N9 fix, turning the lesson into a check rather than a thing to remember. A migration that moves, renames or deletes anything under `.taskless/` makes the files describing that directory wrong from that moment, and it is the only commit that knows. Every other mechanism runs later than the moment the fact changed, which is how `rule-tests/` stayed in the installed README and skill description for two releases after `0005` deleted it. The rule is written at the migration registry, where the next person to add one will be looking, with the three things that follow: update what describes the directory, refresh this repository's own `.taskless/` and commit it, and ask whether already-current projects need a version to reach them at all. The check is `installed-documentation.test.ts`, and it asks the question nothing was asking: is the artifact installed in THIS repository the one this build would write. Not whether a generated file matches its own generator, which is vacuous — whether the copy on disk was refreshed after the template changed, which is the step that was missed. It also names `rule-tests/` and `sg/rules/` directly, so a reintroduction is caught by what it says and not only by a whole-file comparison. Verified by reintroducing the stale sentence and confirming both assertions fail.
1 parent 80013b5 commit 03b2979

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

‎packages/cli/src/filesystem/migrate.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,32 @@ export interface RunMigrationsOptions {
265265
allowVersionMismatches?: boolean;
266266
}
267267

268+
/**
269+
* A MIGRATION OWNS EVERYTHING UNDER `.taskless/`, INCLUDING THE PROSE.
270+
*
271+
* If a migration moves, renames or deletes anything in that directory, the
272+
* files describing the directory are wrong from that moment, and the migration
273+
* is the only commit that knows it. Every other mechanism — a reviewer, a
274+
* linter, someone noticing — runs later than the moment the fact changed.
275+
*
276+
* That is not hypothetical here. `0004` and `0005` relocated every rule and
277+
* deleted `rule-tests/`, and the installed `README.md` and skill description
278+
* went on naming the old tree for two releases. `0001` rewrites the README on
279+
* every run, which sounds like it covers this and does not: migrations only run
280+
* ABOVE the recorded version, so a project that is already current never
281+
* rewrites anything. Reaching those projects took `0006`.
282+
*
283+
* So when you write a migration that changes this directory's shape:
284+
*
285+
* 1. Update whatever describes it. `0001`'s README body derives its layout
286+
* section from the rule layout table, so a table change carries; anything
287+
* written as prose does not.
288+
* 2. Refresh this repository's own `.taskless/` and commit it, since we install
289+
* Taskless on ourselves. `installed-documentation.test.ts` fails if you forget.
290+
* 3. Ask whether already-current projects need the change. If they do, the only
291+
* thing that reaches them is a new version, because nothing below the
292+
* recorded one runs again.
293+
*/
268294
/**
269295
* The schema version a current CLI migrates a project to.
270296
*
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { readFile } from "node:fs/promises";
2+
import { resolve } from "node:path";
3+
4+
import { describe, expect, it } from "vitest";
5+
6+
import { buildReadmeContent } from "../src/filesystem/migrations/0001-init";
7+
import { pinnedSpecifier } from "../src/util/package-manager";
8+
9+
/**
10+
* This repository runs Taskless on itself, so its own `.taskless/` is an
11+
* installed project like any other, and it went stale exactly the way a user's
12+
* would: the README described `sg/rules/` and `rule-tests/` for two migrations
13+
* after `0004` and `0005` dismantled that tree.
14+
*
15+
* WHAT LET IT SIT THERE was that nothing compared the file on disk to what the
16+
* current build would write. Changing the template is a source edit; refreshing
17+
* an already-migrated project is a separate act that nobody was reminded to
18+
* perform, and `runMigrations` returns early on a current project so it never
19+
* happened by itself.
20+
*
21+
* This is the reminder. It is deliberately about OUR copy rather than about the
22+
* generator: asserting that a generated file matches its own generator would be
23+
* vacuous. The question worth asking is whether the installed artifact in this
24+
* repository is the one this build produces.
25+
*/
26+
27+
const repositoryRoot = resolve(import.meta.dirname, "../../..");
28+
29+
describe("this repository's own installed Taskless docs", () => {
30+
it("carries the README this build would write", async () => {
31+
const onDisk = await readFile(
32+
resolve(repositoryRoot, ".taskless", "README.md"),
33+
"utf8"
34+
);
35+
36+
// If this fails, the template changed and this project was not migrated.
37+
// Run `pnpm build && pnpm cli init --no-interactive` and commit the result;
38+
// do not edit `.taskless/README.md` by hand, since the next migration
39+
// overwrites it.
40+
expect(onDisk).toBe(buildReadmeContent(pinnedSpecifier()));
41+
});
42+
43+
it("describes no directory the layout migrations removed", async () => {
44+
// The specific stale words, named so a reintroduction is caught by what it
45+
// says rather than only by a whole-file comparison. `rule-tests/` is the
46+
// directory `0005` deletes.
47+
const onDisk = await readFile(
48+
resolve(repositoryRoot, ".taskless", "README.md"),
49+
"utf8"
50+
);
51+
expect(onDisk).not.toContain("rule-tests");
52+
expect(onDisk).not.toContain("sg/rules/");
53+
});
54+
55+
it("keeps the skill's trigger text off the old layout too", async () => {
56+
// The skill description is what an agent reads before it opens anything, so
57+
// a stale directory name here sends it looking in a place that no longer
58+
// exists. Checked at the SOURCE, which is what `init` installs from.
59+
const skill = await readFile(
60+
resolve(repositoryRoot, "skills", "taskless", "SKILL.md"),
61+
"utf8"
62+
);
63+
expect(skill).not.toContain("rule-tests");
64+
});
65+
});

0 commit comments

Comments
 (0)