@relayfile/adapter-linear 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/discovery/linear/.adapter.md +71 -0
- package/discovery/linear/issues/.create.example.json +6 -0
- package/discovery/linear/issues/.schema.json +145 -0
- package/discovery/linear/issues/{issueId}/comments/.create.example.json +3 -0
- package/discovery/linear/issues/{issueId}/comments/.schema.json +91 -0
- package/dist/__tests__/aliases.test.d.ts +2 -0
- package/dist/__tests__/aliases.test.d.ts.map +1 -0
- package/dist/__tests__/aliases.test.js +184 -0
- package/dist/__tests__/aliases.test.js.map +1 -0
- package/dist/__tests__/by-state.test.d.ts +2 -0
- package/dist/__tests__/by-state.test.d.ts.map +1 -0
- package/dist/__tests__/by-state.test.js +282 -0
- package/dist/__tests__/by-state.test.js.map +1 -0
- package/dist/__tests__/index-emission.test.d.ts +2 -0
- package/dist/__tests__/index-emission.test.d.ts.map +1 -0
- package/dist/__tests__/index-emission.test.js +118 -0
- package/dist/__tests__/index-emission.test.js.map +1 -0
- package/dist/__tests__/layout-prompt.test.d.ts +2 -0
- package/dist/__tests__/layout-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/layout-prompt.test.js +14 -0
- package/dist/__tests__/layout-prompt.test.js.map +1 -0
- package/dist/__tests__/linear-adapter.test.js +241 -7
- package/dist/__tests__/linear-adapter.test.js.map +1 -1
- package/dist/__tests__/name-id-convention.test.d.ts +2 -0
- package/dist/__tests__/name-id-convention.test.d.ts.map +1 -0
- package/dist/__tests__/name-id-convention.test.js +50 -0
- package/dist/__tests__/name-id-convention.test.js.map +1 -0
- package/dist/__tests__/path-mapper.test.js +5 -1
- package/dist/__tests__/path-mapper.test.js.map +1 -1
- package/dist/alias-slug.d.ts +3 -0
- package/dist/alias-slug.d.ts.map +1 -0
- package/dist/alias-slug.js +17 -0
- package/dist/alias-slug.js.map +1 -0
- package/dist/index-emitter.d.ts +10 -0
- package/dist/index-emitter.d.ts.map +1 -0
- package/dist/index-emitter.js +30 -0
- package/dist/index-emitter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layout-prompt.d.ts +7 -0
- package/dist/layout-prompt.d.ts.map +1 -0
- package/dist/layout-prompt.js +45 -0
- package/dist/layout-prompt.js.map +1 -0
- package/dist/linear-adapter.d.ts +11 -0
- package/dist/linear-adapter.d.ts.map +1 -1
- package/dist/linear-adapter.js +403 -33
- package/dist/linear-adapter.js.map +1 -1
- package/dist/path-mapper.d.ts +22 -3
- package/dist/path-mapper.d.ts.map +1 -1
- package/dist/path-mapper.js +133 -21
- package/dist/path-mapper.js.map +1 -1
- package/dist/queries.d.ts +24 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +50 -0
- package/dist/queries.js.map +1 -1
- package/dist/resources.d.ts +25 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +23 -0
- package/dist/resources.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback.d.ts +7 -3
- package/dist/writeback.d.ts.map +1 -1
- package/dist/writeback.js +52 -14
- package/dist/writeback.js.map +1 -1
- package/dist/writeback.test.js +15 -43
- package/dist/writeback.test.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Linear adapter
|
|
2
|
+
|
|
3
|
+
The Linear adapter exposes teams, issues, users, comments, projects, cycles, milestones, and roadmaps under `/linear`, with writeback routes for creating issues and comments.
|
|
4
|
+
|
|
5
|
+
Read-only mounts:
|
|
6
|
+
- `/linear/teams/<teamId>.json` - Team records.
|
|
7
|
+
- `/linear/issues/<issueId>.json` - Issue records.
|
|
8
|
+
- `/linear/users/<userId>.json` - User records.
|
|
9
|
+
- `/linear/comments/<commentId>.json` - Comment records.
|
|
10
|
+
|
|
11
|
+
Resources:
|
|
12
|
+
|
|
13
|
+
| Resource | Schema | Create example | ID pattern | What it does |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| `/linear/issues/<id>.json` | `/linear/issues/.schema.json` | `/linear/issues/.create.example.json` | `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` | Creates a Linear issue. |
|
|
16
|
+
| `/linear/issues/{issueId}/comments/<id>.json` | `/linear/issues/{issueId}/comments/.schema.json` | `/linear/issues/{issueId}/comments/.create.example.json` | `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` | Creates a comment on a Linear issue. |
|
|
17
|
+
|
|
18
|
+
## Operations
|
|
19
|
+
|
|
20
|
+
| To... | Do... |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Read | `cat <id>.json` |
|
|
23
|
+
| Edit | Write a partial JSON object to `<id>.json`. Only included mutable fields PATCH; fields marked `readOnly` in `.schema.json` are rejected. |
|
|
24
|
+
| Create | Write JSON to any non-canonical filename such as `draft-title.json`. The adapter creates the record at `<real-id>.json` and rewrites the draft as `{ "created": "<real-id>", "path": "<resource>/<real-id>.json", "url": "<provider-url>" }`. |
|
|
25
|
+
| Delete | `rm <id>.json` for canonical ids. |
|
|
26
|
+
|
|
27
|
+
## ID Patterns
|
|
28
|
+
- `/linear/issues/<id>.json`: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. Filenames that do not match this pattern are treated as create drafts.
|
|
29
|
+
- `/linear/issues/{issueId}/comments/<id>.json`: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. Filenames that do not match this pattern are treated as create drafts.
|
|
30
|
+
|
|
31
|
+
## Write field contracts
|
|
32
|
+
|
|
33
|
+
### Create Linear issue
|
|
34
|
+
|
|
35
|
+
Resource: `/linear/issues/<id>.json`
|
|
36
|
+
Schema: `/linear/issues/.schema.json`
|
|
37
|
+
Create example: `/linear/issues/.create.example.json`
|
|
38
|
+
Required fields: `teamId`, `title`.
|
|
39
|
+
Optional fields: `description`, `priority`, `assigneeId`, `stateId`, `projectId`, `cycleId`, `labelIds`, `dueDate`, `estimate`, `parentId`.
|
|
40
|
+
|
|
41
|
+
Fields:
|
|
42
|
+
|
|
43
|
+
- `teamId` (required, string, uuid) - Linear team UUID. List `/linear/teams/` to find available teams.
|
|
44
|
+
- `title` (required, string) - Issue title.
|
|
45
|
+
- `description` (optional, string) - Markdown issue body.
|
|
46
|
+
- `priority` (optional, enum) - 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low. Allowed values: `0`, `1`, `2`, `3`, `4`.
|
|
47
|
+
- `assigneeId` (optional, string, uuid) - Linear assignee user UUID.
|
|
48
|
+
- `stateId` (optional, string, uuid) - Linear workflow state UUID.
|
|
49
|
+
- `projectId` (optional, string, uuid) - Linear project UUID.
|
|
50
|
+
- `cycleId` (optional, string, uuid) - Linear cycle UUID.
|
|
51
|
+
- `labelIds` (optional, array) - Linear label UUIDs.
|
|
52
|
+
- `dueDate` (optional, string, date) - Due date in YYYY-MM-DD form.
|
|
53
|
+
- `estimate` (optional, number) - Linear estimate value.
|
|
54
|
+
- `parentId` (optional, string, uuid) - Parent issue UUID.
|
|
55
|
+
|
|
56
|
+
### Create Linear issue comment
|
|
57
|
+
|
|
58
|
+
Resource: `/linear/issues/{issueId}/comments/<id>.json`
|
|
59
|
+
Schema: `/linear/issues/{issueId}/comments/.schema.json`
|
|
60
|
+
Create example: `/linear/issues/{issueId}/comments/.create.example.json`
|
|
61
|
+
Required fields: `body`.
|
|
62
|
+
Optional fields: `parentId`, `doNotSubscribeToIssue`.
|
|
63
|
+
|
|
64
|
+
Fields:
|
|
65
|
+
|
|
66
|
+
- `body` (required, string) - Comment body.
|
|
67
|
+
- `parentId` (optional, string, uuid) - Parent comment UUID for threaded replies.
|
|
68
|
+
- `doNotSubscribeToIssue` (optional, boolean) - Whether to avoid subscribing the commenter to the issue.
|
|
69
|
+
|
|
70
|
+
## Create Examples
|
|
71
|
+
Read the resource `.schema.json` first, then use the sibling `.create.example.json` as a minimal create document. The example intentionally omits read-only fields.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Linear issue",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"teamId",
|
|
7
|
+
"title"
|
|
8
|
+
],
|
|
9
|
+
"properties": {
|
|
10
|
+
"teamId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"format": "uuid",
|
|
13
|
+
"description": "Linear team UUID. List `/linear/teams/` to find available teams."
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Issue title.",
|
|
18
|
+
"minLength": 1
|
|
19
|
+
},
|
|
20
|
+
"description": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Markdown issue body."
|
|
23
|
+
},
|
|
24
|
+
"priority": {
|
|
25
|
+
"enum": [
|
|
26
|
+
0,
|
|
27
|
+
1,
|
|
28
|
+
2,
|
|
29
|
+
3,
|
|
30
|
+
4
|
|
31
|
+
],
|
|
32
|
+
"description": "0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low."
|
|
33
|
+
},
|
|
34
|
+
"assigneeId": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"format": "uuid",
|
|
37
|
+
"description": "Linear assignee user UUID."
|
|
38
|
+
},
|
|
39
|
+
"stateId": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"format": "uuid",
|
|
42
|
+
"description": "Linear workflow state UUID."
|
|
43
|
+
},
|
|
44
|
+
"projectId": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"format": "uuid",
|
|
47
|
+
"description": "Linear project UUID."
|
|
48
|
+
},
|
|
49
|
+
"cycleId": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"format": "uuid",
|
|
52
|
+
"description": "Linear cycle UUID."
|
|
53
|
+
},
|
|
54
|
+
"labelIds": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"description": "Linear label UUIDs.",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"format": "uuid",
|
|
60
|
+
"description": "Linear label UUID."
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"dueDate": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"format": "date",
|
|
66
|
+
"description": "Due date in YYYY-MM-DD form."
|
|
67
|
+
},
|
|
68
|
+
"estimate": {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"description": "Linear estimate value."
|
|
71
|
+
},
|
|
72
|
+
"parentId": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"format": "uuid",
|
|
75
|
+
"description": "Parent issue UUID."
|
|
76
|
+
},
|
|
77
|
+
"id": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Provider canonical record id.",
|
|
80
|
+
"readOnly": true
|
|
81
|
+
},
|
|
82
|
+
"createdAt": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"format": "date-time",
|
|
85
|
+
"description": "Provider creation timestamp.",
|
|
86
|
+
"readOnly": true
|
|
87
|
+
},
|
|
88
|
+
"updatedAt": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"format": "date-time",
|
|
91
|
+
"description": "Provider last update timestamp.",
|
|
92
|
+
"readOnly": true
|
|
93
|
+
},
|
|
94
|
+
"url": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"format": "uri",
|
|
97
|
+
"description": "Provider URL for the record.",
|
|
98
|
+
"readOnly": true
|
|
99
|
+
},
|
|
100
|
+
"identifier": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Provider human-readable identifier or key.",
|
|
103
|
+
"readOnly": true
|
|
104
|
+
},
|
|
105
|
+
"provider": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Relayfile provider name.",
|
|
108
|
+
"readOnly": true
|
|
109
|
+
},
|
|
110
|
+
"objectType": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Relayfile object type.",
|
|
113
|
+
"readOnly": true
|
|
114
|
+
},
|
|
115
|
+
"objectId": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Relayfile object id.",
|
|
118
|
+
"readOnly": true
|
|
119
|
+
},
|
|
120
|
+
"workspaceId": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"description": "Relayfile workspace id.",
|
|
123
|
+
"readOnly": true
|
|
124
|
+
},
|
|
125
|
+
"connectionId": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Relayfile connection id.",
|
|
128
|
+
"readOnly": true
|
|
129
|
+
},
|
|
130
|
+
"_webhook": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"description": "Provider webhook metadata captured during sync.",
|
|
133
|
+
"readOnly": true,
|
|
134
|
+
"additionalProperties": true
|
|
135
|
+
},
|
|
136
|
+
"_connection": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"description": "Relayfile connection metadata captured during sync.",
|
|
139
|
+
"readOnly": true,
|
|
140
|
+
"additionalProperties": true
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"additionalProperties": false,
|
|
144
|
+
"description": "Full resource record schema. Fields marked readOnly are synced from the provider and cannot be written by agents."
|
|
145
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Linear issue comment",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"body"
|
|
7
|
+
],
|
|
8
|
+
"properties": {
|
|
9
|
+
"body": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Comment body.",
|
|
12
|
+
"minLength": 1
|
|
13
|
+
},
|
|
14
|
+
"parentId": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "uuid",
|
|
17
|
+
"description": "Parent comment UUID for threaded replies."
|
|
18
|
+
},
|
|
19
|
+
"doNotSubscribeToIssue": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "Whether to avoid subscribing the commenter to the issue."
|
|
22
|
+
},
|
|
23
|
+
"id": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Provider canonical record id.",
|
|
26
|
+
"readOnly": true
|
|
27
|
+
},
|
|
28
|
+
"createdAt": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"format": "date-time",
|
|
31
|
+
"description": "Provider creation timestamp.",
|
|
32
|
+
"readOnly": true
|
|
33
|
+
},
|
|
34
|
+
"updatedAt": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"format": "date-time",
|
|
37
|
+
"description": "Provider last update timestamp.",
|
|
38
|
+
"readOnly": true
|
|
39
|
+
},
|
|
40
|
+
"url": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"format": "uri",
|
|
43
|
+
"description": "Provider URL for the record.",
|
|
44
|
+
"readOnly": true
|
|
45
|
+
},
|
|
46
|
+
"identifier": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Provider human-readable identifier or key.",
|
|
49
|
+
"readOnly": true
|
|
50
|
+
},
|
|
51
|
+
"provider": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Relayfile provider name.",
|
|
54
|
+
"readOnly": true
|
|
55
|
+
},
|
|
56
|
+
"objectType": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Relayfile object type.",
|
|
59
|
+
"readOnly": true
|
|
60
|
+
},
|
|
61
|
+
"objectId": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Relayfile object id.",
|
|
64
|
+
"readOnly": true
|
|
65
|
+
},
|
|
66
|
+
"workspaceId": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Relayfile workspace id.",
|
|
69
|
+
"readOnly": true
|
|
70
|
+
},
|
|
71
|
+
"connectionId": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Relayfile connection id.",
|
|
74
|
+
"readOnly": true
|
|
75
|
+
},
|
|
76
|
+
"_webhook": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"description": "Provider webhook metadata captured during sync.",
|
|
79
|
+
"readOnly": true,
|
|
80
|
+
"additionalProperties": true
|
|
81
|
+
},
|
|
82
|
+
"_connection": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"description": "Relayfile connection metadata captured during sync.",
|
|
85
|
+
"readOnly": true,
|
|
86
|
+
"additionalProperties": true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"additionalProperties": false,
|
|
90
|
+
"description": "Full resource record schema. Fields marked readOnly are synced from the provider and cannot be written by agents."
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aliases.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/aliases.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { aliasCollisionSuffix, slugifyAlias } from '../alias-slug.js';
|
|
4
|
+
import { LinearAdapter } from '../index.js';
|
|
5
|
+
import { linearByIdAliasPath, linearByTitleAliasPath, linearIssuePath } from '../path-mapper.js';
|
|
6
|
+
function createAdapter() {
|
|
7
|
+
const files = new Map();
|
|
8
|
+
const client = {
|
|
9
|
+
async writeFile(input) {
|
|
10
|
+
files.set(input.path, input.content);
|
|
11
|
+
return { created: true };
|
|
12
|
+
},
|
|
13
|
+
// Sync helper kept for tests that read directly. Accepts (path), (input),
|
|
14
|
+
// or (workspaceId, path) so it works both for the adapter's auxiliary
|
|
15
|
+
// 2-arg readFile path and the alias-emitter's legacy single-arg path call.
|
|
16
|
+
readFile(workspaceIdOrPathOrInput, maybePath) {
|
|
17
|
+
if (typeof workspaceIdOrPathOrInput === 'string') {
|
|
18
|
+
// Adapter passes (workspaceId, path) when readFile.length >= 2.
|
|
19
|
+
// Alias emitter passes a bare path. Distinguish by inspecting the
|
|
20
|
+
// value — paths in this fixture always start with `/`.
|
|
21
|
+
const path = maybePath ?? (workspaceIdOrPathOrInput.startsWith('/') ? workspaceIdOrPathOrInput : undefined);
|
|
22
|
+
return path ? files.get(path) : undefined;
|
|
23
|
+
}
|
|
24
|
+
return files.get(workspaceIdOrPathOrInput.path);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const provider = {
|
|
28
|
+
name: 'linear-test-provider',
|
|
29
|
+
async proxy(_request) {
|
|
30
|
+
return {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: {},
|
|
33
|
+
data: null,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
async healthCheck() {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
adapter: new LinearAdapter(client, provider, {}),
|
|
42
|
+
client,
|
|
43
|
+
files,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe('linear aliases', () => {
|
|
47
|
+
it('writes issue aliases, keeps AGE-8 verbatim in by-id, and updates the parent _index.json', async () => {
|
|
48
|
+
const { adapter, client, files } = createAdapter();
|
|
49
|
+
const event = {
|
|
50
|
+
provider: 'linear',
|
|
51
|
+
eventType: 'issue.create',
|
|
52
|
+
objectType: 'issue',
|
|
53
|
+
objectId: 'issue-123',
|
|
54
|
+
payload: {
|
|
55
|
+
id: 'issue-123',
|
|
56
|
+
identifier: 'AGE-8',
|
|
57
|
+
title: 'Cafe roadmap',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
await adapter.ingestWebhook('ws-linear', event);
|
|
61
|
+
const canonicalPath = '/linear/issues/AGE-8__issue-123.json';
|
|
62
|
+
const byIdPath = linearByIdAliasPath('/linear/issues', 'AGE-8');
|
|
63
|
+
const byTitlePath = linearByTitleAliasPath('/linear/issues', 'Cafe roadmap', 'issue-123');
|
|
64
|
+
assert.ok(files.has(canonicalPath));
|
|
65
|
+
assert.ok(files.has(byIdPath));
|
|
66
|
+
assert.ok(files.has(byTitlePath));
|
|
67
|
+
assert.strictEqual(client.readFile(byIdPath), client.readFile(canonicalPath));
|
|
68
|
+
assert.strictEqual(client.readFile(byTitlePath), client.readFile(canonicalPath));
|
|
69
|
+
// PR 1's writeAuxiliaryFiles overwrites the alias-row `_index.json`
|
|
70
|
+
// emitted by writeLinearAliases with the canonical issue-row array. The
|
|
71
|
+
// alias rows therefore live only transiently inside writeLinearAliases;
|
|
72
|
+
// the durable record reflects the canonical shape.
|
|
73
|
+
const index = JSON.parse(client.readFile('/linear/issues/_index.json') ?? '[]');
|
|
74
|
+
assert.deepStrictEqual(index.map((row) => row.id), ['issue-123']);
|
|
75
|
+
});
|
|
76
|
+
it('writes project by-id aliases from the UUID and disambiguates by-title collisions with an 8-char hash', async () => {
|
|
77
|
+
const { adapter, client } = createAdapter();
|
|
78
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
79
|
+
provider: 'linear',
|
|
80
|
+
eventType: 'project.create',
|
|
81
|
+
objectType: 'project',
|
|
82
|
+
objectId: 'project-1',
|
|
83
|
+
payload: {
|
|
84
|
+
id: 'project-1',
|
|
85
|
+
name: 'Roadmap',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
89
|
+
provider: 'linear',
|
|
90
|
+
eventType: 'project.create',
|
|
91
|
+
objectType: 'project',
|
|
92
|
+
objectId: 'project-2',
|
|
93
|
+
payload: {
|
|
94
|
+
id: 'project-2',
|
|
95
|
+
name: 'Roadmap!!!',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const firstCanonicalPath = '/linear/projects/project-1.json';
|
|
99
|
+
const secondCanonicalPath = '/linear/projects/project-2.json';
|
|
100
|
+
const byIdPath = linearByIdAliasPath('/linear/projects', 'project-1');
|
|
101
|
+
const collisionAliasPath = linearByTitleAliasPath('/linear/projects', 'Roadmap!!!', 'project-2', true);
|
|
102
|
+
assert.strictEqual(client.readFile(byIdPath), client.readFile(firstCanonicalPath));
|
|
103
|
+
assert.strictEqual(client.readFile(collisionAliasPath), client.readFile(secondCanonicalPath));
|
|
104
|
+
assert.ok(collisionAliasPath.endsWith(`${aliasCollisionSuffix('project-2')}.json`));
|
|
105
|
+
});
|
|
106
|
+
it('falls back to the object id for issue by-id aliases when the public identifier is missing', async () => {
|
|
107
|
+
const { adapter, client } = createAdapter();
|
|
108
|
+
const objectId = 'issue-uuid-42';
|
|
109
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
110
|
+
provider: 'linear',
|
|
111
|
+
eventType: 'issue.create',
|
|
112
|
+
objectType: 'issue',
|
|
113
|
+
objectId,
|
|
114
|
+
payload: {
|
|
115
|
+
id: objectId,
|
|
116
|
+
title: 'Roadmap without public ID',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const canonicalPath = linearIssuePath(objectId, 'Roadmap without public ID');
|
|
120
|
+
const byIdPath = linearByIdAliasPath('/linear/issues', objectId);
|
|
121
|
+
assert.strictEqual(client.readFile(byIdPath), client.readFile(canonicalPath));
|
|
122
|
+
});
|
|
123
|
+
it('uses deterministic last-write-wins behavior when two issue payloads collide on the same by-id alias', async () => {
|
|
124
|
+
const { adapter, client } = createAdapter();
|
|
125
|
+
const identifier = 'AGE-8';
|
|
126
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
127
|
+
provider: 'linear',
|
|
128
|
+
eventType: 'issue.create',
|
|
129
|
+
objectType: 'issue',
|
|
130
|
+
objectId: 'issue-123',
|
|
131
|
+
payload: {
|
|
132
|
+
id: 'issue-123',
|
|
133
|
+
identifier,
|
|
134
|
+
title: 'Cafe roadmap',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
138
|
+
provider: 'linear',
|
|
139
|
+
eventType: 'issue.update',
|
|
140
|
+
objectType: 'issue',
|
|
141
|
+
objectId: 'issue-999',
|
|
142
|
+
payload: {
|
|
143
|
+
id: 'issue-999',
|
|
144
|
+
identifier,
|
|
145
|
+
title: 'Renamed roadmap',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// Adapter computes humanReadable via getLinearIssueHumanReadable which
|
|
149
|
+
// prefers `identifier` over title, so canonical filenames here use the
|
|
150
|
+
// shared `AGE-8` prefix and only differ in the trailing id segment.
|
|
151
|
+
const firstCanonicalPath = linearIssuePath('issue-123', identifier);
|
|
152
|
+
const secondCanonicalPath = linearIssuePath('issue-999', identifier);
|
|
153
|
+
const byIdPath = linearByIdAliasPath('/linear/issues', identifier);
|
|
154
|
+
assert.notStrictEqual(client.readFile(firstCanonicalPath), client.readFile(secondCanonicalPath));
|
|
155
|
+
assert.strictEqual(client.readFile(byIdPath), client.readFile(secondCanonicalPath));
|
|
156
|
+
});
|
|
157
|
+
it('writes an untitled by-title alias when an issue title slugs to nothing', async () => {
|
|
158
|
+
const { adapter, client } = createAdapter();
|
|
159
|
+
const objectId = 'issue-emoji-1';
|
|
160
|
+
await adapter.ingestWebhook('ws-linear', {
|
|
161
|
+
provider: 'linear',
|
|
162
|
+
eventType: 'issue.create',
|
|
163
|
+
objectType: 'issue',
|
|
164
|
+
objectId,
|
|
165
|
+
payload: {
|
|
166
|
+
id: objectId,
|
|
167
|
+
identifier: 'AGE-EMOJI',
|
|
168
|
+
title: '🚀🔥',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
// Adapter prefers `identifier` over title via getLinearIssueHumanReadable,
|
|
172
|
+
// so the canonical path uses the identifier-derived slug rather than the
|
|
173
|
+
// emoji title (which would slug to nothing).
|
|
174
|
+
const canonicalPath = linearIssuePath(objectId, 'AGE-EMOJI');
|
|
175
|
+
const byTitlePath = linearByTitleAliasPath('/linear/issues', '🚀🔥', objectId);
|
|
176
|
+
assert.strictEqual(client.readFile(byTitlePath), client.readFile(canonicalPath));
|
|
177
|
+
assert.ok(byTitlePath.endsWith('/by-title/untitled.json'));
|
|
178
|
+
});
|
|
179
|
+
it('slugging is deterministic, ASCII-folded, and strips traversal characters', () => {
|
|
180
|
+
assert.strictEqual(slugifyAlias('Café ../ Roadmap'), 'cafe-roadmap');
|
|
181
|
+
assert.strictEqual(slugifyAlias('Café ../ Roadmap'), slugifyAlias('Café ../ Roadmap'));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
//# sourceMappingURL=aliases.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aliases.test.js","sourceRoot":"","sources":["../../src/__tests__/aliases.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"by-state.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/by-state.test.ts"],"names":[],"mappings":""}
|