@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.
Files changed (70) hide show
  1. package/discovery/linear/.adapter.md +71 -0
  2. package/discovery/linear/issues/.create.example.json +6 -0
  3. package/discovery/linear/issues/.schema.json +145 -0
  4. package/discovery/linear/issues/{issueId}/comments/.create.example.json +3 -0
  5. package/discovery/linear/issues/{issueId}/comments/.schema.json +91 -0
  6. package/dist/__tests__/aliases.test.d.ts +2 -0
  7. package/dist/__tests__/aliases.test.d.ts.map +1 -0
  8. package/dist/__tests__/aliases.test.js +184 -0
  9. package/dist/__tests__/aliases.test.js.map +1 -0
  10. package/dist/__tests__/by-state.test.d.ts +2 -0
  11. package/dist/__tests__/by-state.test.d.ts.map +1 -0
  12. package/dist/__tests__/by-state.test.js +282 -0
  13. package/dist/__tests__/by-state.test.js.map +1 -0
  14. package/dist/__tests__/index-emission.test.d.ts +2 -0
  15. package/dist/__tests__/index-emission.test.d.ts.map +1 -0
  16. package/dist/__tests__/index-emission.test.js +118 -0
  17. package/dist/__tests__/index-emission.test.js.map +1 -0
  18. package/dist/__tests__/layout-prompt.test.d.ts +2 -0
  19. package/dist/__tests__/layout-prompt.test.d.ts.map +1 -0
  20. package/dist/__tests__/layout-prompt.test.js +14 -0
  21. package/dist/__tests__/layout-prompt.test.js.map +1 -0
  22. package/dist/__tests__/linear-adapter.test.js +241 -7
  23. package/dist/__tests__/linear-adapter.test.js.map +1 -1
  24. package/dist/__tests__/name-id-convention.test.d.ts +2 -0
  25. package/dist/__tests__/name-id-convention.test.d.ts.map +1 -0
  26. package/dist/__tests__/name-id-convention.test.js +50 -0
  27. package/dist/__tests__/name-id-convention.test.js.map +1 -0
  28. package/dist/__tests__/path-mapper.test.js +5 -1
  29. package/dist/__tests__/path-mapper.test.js.map +1 -1
  30. package/dist/alias-slug.d.ts +3 -0
  31. package/dist/alias-slug.d.ts.map +1 -0
  32. package/dist/alias-slug.js +17 -0
  33. package/dist/alias-slug.js.map +1 -0
  34. package/dist/index-emitter.d.ts +10 -0
  35. package/dist/index-emitter.d.ts.map +1 -0
  36. package/dist/index-emitter.js +30 -0
  37. package/dist/index-emitter.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/layout-prompt.d.ts +7 -0
  43. package/dist/layout-prompt.d.ts.map +1 -0
  44. package/dist/layout-prompt.js +45 -0
  45. package/dist/layout-prompt.js.map +1 -0
  46. package/dist/linear-adapter.d.ts +11 -0
  47. package/dist/linear-adapter.d.ts.map +1 -1
  48. package/dist/linear-adapter.js +403 -33
  49. package/dist/linear-adapter.js.map +1 -1
  50. package/dist/path-mapper.d.ts +22 -3
  51. package/dist/path-mapper.d.ts.map +1 -1
  52. package/dist/path-mapper.js +133 -21
  53. package/dist/path-mapper.js.map +1 -1
  54. package/dist/queries.d.ts +24 -0
  55. package/dist/queries.d.ts.map +1 -1
  56. package/dist/queries.js +50 -0
  57. package/dist/queries.js.map +1 -1
  58. package/dist/resources.d.ts +25 -0
  59. package/dist/resources.d.ts.map +1 -0
  60. package/dist/resources.js +23 -0
  61. package/dist/resources.js.map +1 -0
  62. package/dist/types.d.ts +2 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/writeback.d.ts +7 -3
  65. package/dist/writeback.d.ts.map +1 -1
  66. package/dist/writeback.js +52 -14
  67. package/dist/writeback.js.map +1 -1
  68. package/dist/writeback.test.js +15 -43
  69. package/dist/writeback.test.js.map +1 -1
  70. 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,6 @@
1
+ {
2
+ "teamId": "00000000-0000-0000-0000-000000000000",
3
+ "title": "Replace example title",
4
+ "description": "Optional markdown body.",
5
+ "priority": 0
6
+ }
@@ -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,3 @@
1
+ {
2
+ "body": "Replace example comment body."
3
+ }
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=aliases.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=by-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"by-state.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/by-state.test.ts"],"names":[],"mappings":""}