Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .agents/pm/chores/pm-linear-itg9.toon
Original file line number Diff line number Diff line change
@@ -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: ""
2 changes: 2 additions & 0 deletions .agents/pm/history/pm-linear-itg9.jsonl
Original file line number Diff line number Diff line change
@@ -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"}
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- Adopt pm-cli 2026.7.27 ([pm-linear-zntu](https://github.com/unbraind/pm-linear/blob/main/.agents/pm/chores/pm-linear-zntu.toon))
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

## 2026.7.27 - 2026-07-27
Expand Down
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2249,7 +2249,7 @@ const defineExtension = <TModule extends ExtensionModule>(module: TModule): TMod

export default defineExtension({
name: "pm-linear",
version: "2026.7.27",
version: "2026.7.28",

activate(api: ExtensionApi) {
// -----------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
54 changes: 19 additions & 35 deletions test/atomic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<any> {
let handler: ((ctx: any) => Promise<any>) | 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<any>;
// 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<ExtensionTestHarness> {
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,
Expand Down
64 changes: 30 additions & 34 deletions test/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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", () => {
Expand Down