diff --git a/.agents/pm/chores/pm-linear-itg9.toon b/.agents/pm/chores/pm-linear-itg9.toon new file mode 100644 index 00000000..8f190c7d --- /dev/null +++ b/.agents/pm/chores/pm-linear-itg9.toon @@ -0,0 +1,13 @@ +id: pm-linear-itg9 +title: Adopt pm-cli 2026.7.28 and migrate activation tests to the real SDK harness +description: "" +type: Chore +status: closed +priority: 2 +tags: [] +created_at: "2026-07-28T03:40:12.051Z" +updated_at: "2026-07-28T03:40:16.529Z" +closed_at: "2026-07-28T03:40:16.529Z" +author: pi-agent +close_reason: Bumped pm-cli to 2026.7.28 and replaced hand-rolled api doubles in test/smoke.test.ts and test/atomic.test.ts with createExtensionTestHarness so the real activation engine validates registration and importer dispatch. Release check passes. +body: "" diff --git a/.agents/pm/history/pm-linear-itg9.jsonl b/.agents/pm/history/pm-linear-itg9.jsonl new file mode 100644 index 00000000..be5f95a9 --- /dev/null +++ b/.agents/pm/history/pm-linear-itg9.jsonl @@ -0,0 +1,2 @@ +{"ts":"2026-07-28T03:40:12.051Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_provenance":{"model":null},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-linear-itg9"},{"op":"add","path":"/metadata/title","value":"Adopt pm-cli 2026.7.28 and migrate activation tests to the real SDK harness"},{"op":"add","path":"/metadata/description","value":""},{"op":"add","path":"/metadata/type","value":"Chore"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-07-28T03:40:12.051Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-28T03:40:12.051Z"},{"op":"add","path":"/metadata/author","value":"pi-agent"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"0c31e1a7e664a7458a19597c420c06bed0be9281884bc954f202bd9636bc6091","message":""} +{"ts":"2026-07-28T03:40:16.529Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_provenance":{"model":null},"op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-28T03:40:16.529Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-07-28T03:40:16.529Z"},{"op":"add","path":"/metadata/close_reason","value":"Bumped pm-cli to 2026.7.28 and replaced hand-rolled api doubles in test/smoke.test.ts and test/atomic.test.ts with createExtensionTestHarness so the real activation engine validates registration and importer dispatch. Release check passes."}],"before_hash":"0c31e1a7e664a7458a19597c420c06bed0be9281884bc954f202bd9636bc6091","after_hash":"b85c30b7cdad8b40d41f005c6074bfb6fbd96e573c8dc6d74b66e31d29ceccfd"} diff --git a/.gitattributes b/.gitattributes index 119c7f76..c3e7c664 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,5 +24,5 @@ ".agents/pm/**/*.jsonl" merge=pm-relationship ".agents/pm/history/*.jsonl" merge=pm-history ".agents/pm/settings.json" merge=pm-json -".agents/pm/schema/*.json" merge=pm-json +".agents/pm/**/*.json" merge=pm-json # pm-cli:merge-drivers:end diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf0ea19..dad412d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## Unreleased +## 2026.7.28 - 2026-07-28 ### Other +- Adopt pm-cli 2026.7.28 and migrate activation tests to the real SDK harness ([pm-linear-itg9](https://github.com/unbraind/pm-linear/blob/main/.agents/pm/chores/pm-linear-itg9.toon)) +- Eliminate all source any with real Linear GraphQL and SDK handler types ([pm-linear-pfmo](https://github.com/unbraind/pm-linear/blob/main/.agents/pm/chores/pm-linear-pfmo.toon)) - Adopt pm-cli 2026.7.27 ([pm-linear-zntu](https://github.com/unbraind/pm-linear/blob/main/.agents/pm/chores/pm-linear-zntu.toon)) ## 2026.7.27 - 2026-07-27 diff --git a/index.ts b/index.ts index 803a088c..eb735d0e 100644 --- a/index.ts +++ b/index.ts @@ -2249,7 +2249,7 @@ const defineExtension = (module: TModule): TMod export default defineExtension({ name: "pm-linear", - version: "2026.7.27", + version: "2026.7.28", activate(api: ExtensionApi) { // ----------------------------------------------------------------------- diff --git a/manifest.json b/manifest.json index 59dea5d8..5663e33b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "pm-linear", - "version": "2026.7.27", + "version": "2026.7.28", "description": "Linear.app issue sync for pm-cli", "author": "@unbraind", "entry": "./dist/index.js", diff --git a/package-lock.json b/package-lock.json index fcac53f1..af445be0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "pm-linear", - "version": "2026.7.27", + "version": "2026.7.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pm-linear", - "version": "2026.7.27", + "version": "2026.7.28", "license": "MIT", "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "^2026.7.27", + "@unbrained/pm-cli": "^2026.7.28", "pm-changelog": "^2026.7.25", "typescript": "^7.0.2" }, @@ -18,7 +18,7 @@ "node": ">=22.18.0" }, "peerDependencies": { - "@unbrained/pm-cli": ">=2026.7.27" + "@unbrained/pm-cli": ">=2026.7.28" } }, "node_modules/@apm-js-collab/code-transformer": { @@ -353,9 +353,9 @@ } }, "node_modules/@toon-format/toon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz", - "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-4.1.0.tgz", + "integrity": "sha512-dBB3pkEx9QYvHnHR6rtkaBAh+7x4W/oA5ONur4G0fh7Ow69PbPuM7OFxzNRABqyxC0t6SZ3RixiGbCuaFjPDAQ==", "dev": true, "license": "MIT" }, @@ -717,14 +717,14 @@ } }, "node_modules/@unbrained/pm-cli": { - "version": "2026.7.27", - "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.7.27.tgz", - "integrity": "sha512-18OEQkGXVAd71yJ6iuZKojl29ypqqlN0jJYFAcs+VBsM7EH45wrOP/7C3jrPW4pZWF4fJ5M6VGYOC0a358nh9w==", + "version": "2026.7.28", + "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.7.28.tgz", + "integrity": "sha512-tuzU4q0e5qKzfXPe6MsBxWeZM3COHCGAFegxsOArumYldbvAPZpbaLnWf82wogl/LcRa6RJ/awK7kWgpb2UpCw==", "dev": true, "license": "MIT", "dependencies": { "@sentry/node": "10.67.0", - "@toon-format/toon": "^2.3.1", + "@toon-format/toon": "^4.1.0", "@types/node": ">=22", "commander": "^15.0.0", "fast-glob": "^3.3.3", diff --git a/package.json b/package.json index 5666c3d8..f81636af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pm-linear", - "version": "2026.7.27", + "version": "2026.7.28", "description": "Linear.app issue sync for pm-cli", "repository": { "type": "git", @@ -39,11 +39,11 @@ "merge:install": "pm merge install" }, "peerDependencies": { - "@unbrained/pm-cli": ">=2026.7.27" + "@unbrained/pm-cli": ">=2026.7.28" }, "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "^2026.7.27", + "@unbrained/pm-cli": "^2026.7.28", "pm-changelog": "^2026.7.25", "typescript": "^7.0.2" }, diff --git a/test/atomic.test.ts b/test/atomic.test.ts index 0cbdae81..8390bc51 100644 --- a/test/atomic.test.ts +++ b/test/atomic.test.ts @@ -15,6 +15,7 @@ import extension, { syncLinearIssues, } from "../dist/index.js"; import type { PreparedLinearImport, LinearIssue } from "../dist/index.js"; +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; const PM_BIN = process.platform === "win32" ? "pm.cmd" : "pm"; const PM_SPAWN_OPTS = { @@ -661,51 +662,34 @@ test("a settings read fault aborts before commit rather than forking identity", // --------------------------------------------------------------------------- // the linear-sync importer honors --dry-run (never turns a preview into a write) // --------------------------------------------------------------------------- -function captureImporter(name: string): (ctx: any) => Promise { - let handler: ((ctx: any) => Promise) | undefined; - const noop = (): void => {}; - const api: any = { - registerCommand: noop, - registerParser: noop, - registerPreflight: noop, - registerService: noop, - registerFlags: noop, - registerItemFields: noop, - registerItemTypes: noop, - registerMigration: noop, - registerRenderer: noop, - registerImporter: (registeredName: string, registeredHandler: any) => { - if (registeredName === name) handler = registeredHandler; - }, - registerExporter: noop, - registerSearchProvider: noop, - registerVectorStoreAdapter: noop, - hooks: { - beforeCommand: noop, - afterCommand: noop, - onWrite: noop, - onRead: noop, - onIndex: noop, - }, - }; - extension.activate(api); - assert.ok(handler, `expected importer "${name}" to be registered`); - return handler as (ctx: any) => Promise; +// Activated once through pm's REAL engine via createExtensionTestHarness so the +// importer is exercised through real dispatch (runImporter), not a hand-rolled +// api double that registers the handler and then never evaluates it. +let atomicHarness: ExtensionTestHarness | undefined; +async function getAtomicHarness(): Promise { + if (!atomicHarness) { + atomicHarness = await createExtensionTestHarness(extension, { + name: "pm-linear", + capabilities: ["commands", "schema", "importers", "preflight"], + }); + assert.deepEqual(atomicHarness.activation.failed, [], "activation must not fail"); + } + return atomicHarness; } test("linear-sync importer honors --dry-run and never writes", async () => { - const importer = captureImporter("linear-sync"); + const harness = await getAtomicHarness(); const root = freshTracker(); try { // Plain --dry-run must take the offline preview path: no network, and no // workspace write. Before the fix, the importer dropped dryRun and fell // through to syncLinearIssues, which writes one pm item per issue. - const result = await importer({ + const { result } = await harness.runImporter({ + importer: "linear-sync", options: { team: "ENG", "dry-run": true }, - pm_root: root, - global: { json: true }, + pmRoot: root, }); - assert.strictEqual(result.dryRun, true, "dry-run result must be flagged"); + assert.strictEqual((result as any).dryRun, true, "dry-run result must be flagged"); assert.strictEqual( itemCount(root), 0, diff --git a/test/smoke.test.ts b/test/smoke.test.ts index a6506ea9..e8af6d2a 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,6 +1,8 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { createExtensionTestHarness, type ExtensionTestHarness } from "@unbrained/pm-cli/sdk/testing"; + import extension, { parseStatusMap, resolveStatus, @@ -43,40 +45,34 @@ test("extension has required shape", () => { assert.strictEqual(typeof extension.activate, "function", "activate should be a function"); }); -test("extension registers expected capabilities", () => { - const registered: string[] = []; - // Mirror the full ExtensionApi the activate() body actually touches; a partial - // mock would throw TypeError when the extension calls an absent method. - const api = { - registerCommand: () => { registered.push("command"); }, - registerParser: () => { registered.push("parser"); }, - registerPreflight: () => { registered.push("preflight"); }, - registerService: () => { registered.push("service"); }, - registerFlags: () => { registered.push("flags"); }, - registerItemFields: () => { registered.push("itemFields"); }, - registerItemTypes: () => { registered.push("itemTypes"); }, - registerMigration: () => { registered.push("migration"); }, - registerRenderer: () => { registered.push("renderer"); }, - registerImporter: () => { registered.push("importer"); }, - registerExporter: () => { registered.push("exporter"); }, - registerSearchProvider: () => { registered.push("search"); }, - registerVectorStoreAdapter: () => { registered.push("vector"); }, - hooks: { - beforeCommand: () => { registered.push("beforeCommand"); }, - afterCommand: () => { registered.push("afterCommand"); }, - onWrite: () => { registered.push("onWrite"); }, - onRead: () => { registered.push("onRead"); }, - onIndex: () => { registered.push("onIndex"); }, - }, - }; - extension.activate(api as any); - // sync command, schema fields, the new linear importer/exporter, legacy importer - assert.ok(registered.includes("command"), "should register the sync command"); - assert.ok(registered.includes("itemFields"), "should register item fields"); - assert.ok(registered.includes("importer"), "should register importer(s)"); - assert.ok(registered.includes("exporter"), "should register the exporter"); - assert.ok(registered.includes("preflight"), "should register the preflight guard"); - assert.ok(registered.includes("flags"), "should register importer/exporter flags"); +// --------------------------------------------------------------------------- +// Activation proof: drive the extension through pm's REAL registration +// validation and activation engine via createExtensionTestHarness, so a host +// rejection (e.g. a host-owned flag collision that aborts command registration) +// fails this suite instead of staying green against a hand-rolled api double. +// --------------------------------------------------------------------------- + +let linearHarness: ExtensionTestHarness; + +test("extension activates cleanly and registers expected capabilities", async () => { + linearHarness = await createExtensionTestHarness(extension, { + name: "pm-linear", + capabilities: ["commands", "schema", "importers", "preflight"], + }); + assert.deepEqual(linearHarness.activation.failed, [], "activation must not fail"); + // sync + validate commands + linearHarness.assertCommandContract({ command: "linear sync" }); + linearHarness.assertCommandContract({ command: "linear validate" }); + // schema item fields + linearHarness.assertItemField({ field: "linear_id", type: "string" }); + linearHarness.assertItemField({ field: "linear_url", type: "string" }); + // native + legacy importers, exporter, preflight guard, importer/exporter flags + linearHarness.assertImporter({ importer: "linear" }); + linearHarness.assertImporter({ importer: "linear-sync" }); + linearHarness.assertExporter({ exporter: "linear" }); + linearHarness.assertPreflightOverride(); + linearHarness.assertFlags({ targetCommand: "linear import", flags: ["--team", "--dry-run"] }); + linearHarness.assertFlags({ targetCommand: "linear export", flags: ["--push", "--dry-run"] }); }); test("parseStatusMap preserves original key casing and ignores junk", () => {