|
1 | 1 | import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; |
2 | 2 | import { tmpdir } from "node:os"; |
3 | | -import { join } from "node:path"; |
| 3 | +import { dirname, join } from "node:path"; |
4 | 4 | import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
5 | 5 |
|
| 6 | +import { buildSkillStub } from "../../src/install/canonical"; |
6 | 7 | import { parseFrontmatter } from "../../src/install/frontmatter"; |
7 | 8 | import { |
8 | 9 | applyInstallPlan, |
@@ -98,13 +99,54 @@ describe("installing a nightly writes a stub description that names no CLI packa |
98 | 99 | expect(canonical).not.toContain("npx @taskless/cli agent"); |
99 | 100 | }); |
100 | 101 |
|
101 | | - it("a second install pinned to a different nightly leaves the stub unchanged", async () => { |
| 102 | + it("a second install under the SAME pin is idempotent: nothing gets rewritten", async () => { |
102 | 103 | await install(); |
103 | 104 | const first = await readFile(skillStubPath(), "utf8"); |
104 | 105 |
|
105 | 106 | // Re-running install (same build, so same pin) must not rewrite a stub |
106 | | - // whose description never carried a version to begin with. |
| 107 | + // whose description never carried a version to begin with. The genuine |
| 108 | + // cross-pin case — an EARLIER, differently pinned install's stub — is |
| 109 | + // covered separately below, since a single compile-time define can't |
| 110 | + // produce two different pins within one test run. |
107 | 111 | await install(); |
108 | 112 | expect(await readFile(skillStubPath(), "utf8")).toBe(first); |
109 | 113 | }); |
| 114 | + |
| 115 | + it("a stub frozen by an EARLIER, differently pinned nightly converges on the next install", async () => { |
| 116 | + // Simulates the actual defect in taskless/cli#298: a project installed an |
| 117 | + // older nightly, pinned to a DIFFERENT version, back when a skill's |
| 118 | + // `description` still carried a baked-in CLI invocation. That earlier |
| 119 | + // install's stub is not reachable by calling `install()` twice under this |
| 120 | + // file's single fixed `__TASKLESS_CLI__` define (a compile-time Vite |
| 121 | + // define can't vary within one test run), so it is hand-crafted here with |
| 122 | + // `buildSkillStub` instead — the same builder `writeSkill` itself calls, |
| 123 | + // fed the OLD-style description a pre-fix source file would have produced. |
| 124 | + await mkdir(dirname(skillStubPath()), { recursive: true }); |
| 125 | + const frozenByEarlierNightly = buildSkillStub({ |
| 126 | + name: "taskless", |
| 127 | + description: |
| 128 | + "Use for any Taskless task. Fetch recipes via " + |
| 129 | + "`npx @taskless/cli-nightly@0.0.0-nightly.previous agent route`.", |
| 130 | + }); |
| 131 | + await writeFile(skillStubPath(), frozenByEarlierNightly, "utf8"); |
| 132 | + |
| 133 | + await install(); |
| 134 | + |
| 135 | + const rewritten = await readFile(skillStubPath(), "utf8"); |
| 136 | + const rewrittenDescription = parseFrontmatter(rewritten).data |
| 137 | + .description as string; |
| 138 | + |
| 139 | + // The stub actually changed — this build's install converged it rather |
| 140 | + // than leaving the earlier nightly's frozen copy in place. |
| 141 | + expect(rewritten).not.toBe(frozenByEarlierNightly); |
| 142 | + // It converged onto the CURRENT source description (invocation-free), not |
| 143 | + // merely onto some other pin. |
| 144 | + const currentDescription = getEmbeddedSkills().find( |
| 145 | + (s) => s.name === "taskless" |
| 146 | + )?.description; |
| 147 | + expect(rewrittenDescription).toBe(currentDescription); |
| 148 | + // And, the property the whole fix establishes: no pinned or unpinned CLI |
| 149 | + // package reference survives, from either the old install or this one. |
| 150 | + expect(rewrittenDescription).not.toMatch(/@taskless\/cli/); |
| 151 | + }); |
110 | 152 | }); |
0 commit comments