@adea-ai/plugins 3.0.10 → 4.0.1
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/README.md +18 -11
- package/dist/agent-plugins-catalog.d.ts +2 -3
- package/dist/agent-plugins-catalog.js +2 -10
- package/dist/agent-plugins-catalog.js.map +1 -1
- package/dist/catalog-store.d.ts +81 -0
- package/dist/catalog-store.js +487 -0
- package/dist/catalog-store.js.map +1 -0
- package/dist/consumer-index.d.ts +1 -1
- package/dist/consumer-index.js +17 -7
- package/dist/consumer-index.js.map +1 -1
- package/dist/display-name.js +3 -2
- package/dist/display-name.js.map +1 -1
- package/dist/harness.d.ts +50 -9
- package/dist/harness.js +190 -147
- package/dist/harness.js.map +1 -1
- package/dist/icon-mirror.d.ts +2 -2
- package/dist/icon-mirror.js +38 -25
- package/dist/icon-mirror.js.map +1 -1
- package/dist/icons.js +5 -27
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +103 -88
- package/dist/index.js.map +1 -1
- package/dist/publication.d.ts +8 -11
- package/dist/publication.js +8 -11
- package/dist/publication.js.map +1 -1
- package/dist/schema.d.ts +0 -40
- package/dist/schema.js +0 -36
- package/dist/schema.js.map +1 -1
- package/dist/site-icons.d.ts +2 -0
- package/dist/site-icons.js +46 -9
- package/dist/site-icons.js.map +1 -1
- package/dist/source-digest.d.ts +2 -0
- package/dist/source-digest.js +12 -0
- package/dist/source-digest.js.map +1 -0
- package/dist/sources.js +3 -3
- package/dist/sources.js.map +1 -1
- package/native/build-provenance.txt +5 -5
- package/native/staged-reader-win32-arm64.node +0 -0
- package/native/staged-reader-win32-x64.node +0 -0
- package/package.json +4 -9
- package/src/agent-plugins-catalog.ts +3 -10
- package/src/artifacts.test.ts +80 -0
- package/src/catalog-installation.test.ts +1 -1
- package/src/catalog-store.ts +546 -0
- package/src/consumer-index.test.ts +125 -0
- package/src/consumer-index.ts +23 -8
- package/src/default-head.test.ts +143 -0
- package/src/display-name.test.ts +8 -0
- package/src/display-name.ts +3 -2
- package/src/{agent-plugins-harness.test.ts → harness.test.ts} +1 -1
- package/src/harness.ts +249 -170
- package/src/icon-mirror.ts +36 -23
- package/src/icons.test.ts +5 -0
- package/src/icons.ts +6 -29
- package/src/index.test.ts +272 -40
- package/src/index.ts +131 -94
- package/src/manifest-retry.test.ts +49 -0
- package/src/portable-catalog.test.ts +8 -5
- package/src/publication.ts +8 -11
- package/src/schema.ts +0 -40
- package/src/site-icon-fallback.test.ts +36 -19
- package/src/site-icons.test.ts +104 -0
- package/src/site-icons.ts +47 -9
- package/src/source-config-sync.test.ts +84 -0
- package/src/source-digest.test.ts +101 -0
- package/src/source-digest.ts +14 -0
- package/src/sources-json.test.ts +53 -0
- package/src/sources.ts +3 -3
- package/src/upstream-headers.test.ts +31 -7
- package/src/write-artifacts.test.ts +375 -0
- package/dist/agent-plugins-harness.d.ts +0 -54
- package/dist/agent-plugins-harness.js +0 -212
- package/dist/agent-plugins-harness.js.map +0 -1
- package/src/agent-plugins-harness.ts +0 -281
package/src/icons.ts
CHANGED
|
@@ -73,42 +73,20 @@ const MARK_WORDS = new Set([
|
|
|
73
73
|
'logomark',
|
|
74
74
|
'wordmark',
|
|
75
75
|
])
|
|
76
|
-
const VARIANT_WORDS = new Set([
|
|
77
|
-
'large',
|
|
78
|
-
'small',
|
|
79
|
-
'light',
|
|
80
|
-
'dark',
|
|
81
|
-
'mono',
|
|
82
|
-
'white',
|
|
83
|
-
'black',
|
|
84
|
-
'positive',
|
|
85
|
-
'negative',
|
|
86
|
-
'color',
|
|
87
|
-
'colour',
|
|
88
|
-
'primary',
|
|
89
|
-
'secondary',
|
|
90
|
-
'horizontal',
|
|
91
|
-
'vertical',
|
|
92
|
-
'square',
|
|
93
|
-
'flat',
|
|
94
|
-
'glyph',
|
|
95
|
-
])
|
|
96
|
-
|
|
97
76
|
/**
|
|
98
77
|
* Splits a file name into mark tokens. Separators are normalised because
|
|
99
78
|
* vendors write marks as `logo.svg`, `atlan-logo.png`, `Dynatrace_Logo_color_positive.png`
|
|
100
79
|
* or `cs logo big.png` alike.
|
|
101
80
|
*/
|
|
102
|
-
function splitName(basename: string): {
|
|
81
|
+
function splitName(basename: string): { stemRank: number | undefined; size: number } {
|
|
103
82
|
const withoutExtension = basename.slice(0, basename.lastIndexOf('.'))
|
|
104
83
|
const words = withoutExtension
|
|
105
84
|
.split(/[\s._-]+/)
|
|
106
85
|
.filter(Boolean)
|
|
107
86
|
.map((word) => word.toLowerCase())
|
|
108
87
|
const size = words.some((word) => word === 'small') ? 1 : 0
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
return { stems, marks, size }
|
|
88
|
+
const ranks = words.filter((word) => MARK_WORDS.has(word)).map((word) => STEM_RANK[word] ?? 0)
|
|
89
|
+
return { stemRank: ranks.length > 0 ? Math.min(...ranks) : undefined, size }
|
|
112
90
|
}
|
|
113
91
|
|
|
114
92
|
/**
|
|
@@ -133,9 +111,7 @@ export function selectIconPath(fileIndex: readonly string[]): string | null {
|
|
|
133
111
|
const directories = segments.map((segment) => segment.toLowerCase())
|
|
134
112
|
if (directories.some((segment) => EXCLUDED.test(segment))) continue
|
|
135
113
|
if (directories.some((segment) => EXCLUDED_DIRECTORIES.includes(segment))) continue
|
|
136
|
-
const {
|
|
137
|
-
const single = stems.length === 1 ? stems[0]! : undefined
|
|
138
|
-
const stemRank = marks ? (single ? (STEM_RANK[single] ?? 0) : 0) : undefined
|
|
114
|
+
const { stemRank, size } = splitName(basename)
|
|
139
115
|
const nested = directories.filter((segment) => DIRECTORY_RANK[segment] !== undefined)
|
|
140
116
|
// A mark named as one — `logo.svg`, `atlan-logo.png`, `cs_logo_large.png` —
|
|
141
117
|
// is accepted anywhere, including the plugin root. An unnamed image must sit
|
|
@@ -149,7 +125,8 @@ export function selectIconPath(fileIndex: readonly string[]): string | null {
|
|
|
149
125
|
: 0
|
|
150
126
|
if (!Number.isFinite(directoryRank)) continue
|
|
151
127
|
const score = [stemRank ?? 5, directoryRank, size, EXTENSION_RANK[extension] ?? 9, path.length]
|
|
152
|
-
|
|
128
|
+
const comparison = best ? compareScore(score, best.score) : -1
|
|
129
|
+
if (!best || comparison < 0 || (comparison === 0 && path < best.path)) best = { path, score }
|
|
153
130
|
}
|
|
154
131
|
return best ? best.path : null
|
|
155
132
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { chmod, mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
2
3
|
import { readdirSync, readFileSync } from 'node:fs'
|
|
3
4
|
import { gzipSync } from 'node:zlib'
|
|
4
5
|
import { pack } from 'tar-stream'
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
type CategoryMap,
|
|
22
23
|
type ProductAliases,
|
|
23
24
|
type ResolvedSource,
|
|
25
|
+
type GeneratedArtifacts,
|
|
24
26
|
} from './index.js'
|
|
25
27
|
import {
|
|
26
28
|
parseJsonDocument,
|
|
@@ -28,7 +30,6 @@ import {
|
|
|
28
30
|
parseSourceSpec,
|
|
29
31
|
type SourceConfig,
|
|
30
32
|
} from './sources.js'
|
|
31
|
-
import { createMaterializationPlan, supportedHarnesses } from './harness.js'
|
|
32
33
|
|
|
33
34
|
const sources: SourceConfig[] = [
|
|
34
35
|
{
|
|
@@ -409,6 +410,232 @@ describe('deterministic catalog synchronization', () => {
|
|
|
409
410
|
).rejects.toThrow('SOURCE_LOCK_PLUGIN_PIN_MISMATCH: pin-test:remote')
|
|
410
411
|
})
|
|
411
412
|
|
|
413
|
+
test('refreshes explicit external revisions and repositories outside lock replay', async () => {
|
|
414
|
+
const fixtureRoot = await mkdtemp(join(tmpdir(), 'plugin-pin-refresh-'))
|
|
415
|
+
try {
|
|
416
|
+
await mkdir(join(fixtureRoot, 'pin-test'), { recursive: true })
|
|
417
|
+
const manifestPath = join(fixtureRoot, 'pin-test', 'marketplace.json')
|
|
418
|
+
const writeManifest = (sha: string, url = 'https://github.com/acme/remote-plugin') =>
|
|
419
|
+
writeFile(
|
|
420
|
+
manifestPath,
|
|
421
|
+
JSON.stringify({
|
|
422
|
+
plugins: [
|
|
423
|
+
{
|
|
424
|
+
name: 'remote',
|
|
425
|
+
description: 'Remote fixture plugin.',
|
|
426
|
+
source: { source: 'git', url, sha },
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
const source: SourceConfig = {
|
|
432
|
+
...sources[0]!,
|
|
433
|
+
sourceId: 'pin-test',
|
|
434
|
+
manifestPath: 'marketplace.json',
|
|
435
|
+
fixturePath: 'pin-test',
|
|
436
|
+
fixtureCommitSha: '5'.repeat(40),
|
|
437
|
+
}
|
|
438
|
+
const options = {
|
|
439
|
+
sources: [source],
|
|
440
|
+
categoryMap,
|
|
441
|
+
productAliases,
|
|
442
|
+
policy,
|
|
443
|
+
mode: 'offline' as const,
|
|
444
|
+
fixtureRoot,
|
|
445
|
+
snapshotLoader: {
|
|
446
|
+
load: async () => ({
|
|
447
|
+
files: new Map([['plugin.json', new TextEncoder().encode('{"name":"Remote"}')]]),
|
|
448
|
+
symlinks: [],
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
await writeManifest('6'.repeat(40))
|
|
453
|
+
const first = await synchronize(options)
|
|
454
|
+
await writeManifest('7'.repeat(40))
|
|
455
|
+
const refreshed = await synchronize({ ...options, existingLock: first.lock! })
|
|
456
|
+
expect(refreshed.lock?.sources[0]?.pluginPins[0]?.resolvedCommitSha).toBe('7'.repeat(40))
|
|
457
|
+
expect(refreshed.catalog?.plugins[0]?.availableReleases[0]?.resolvedCommitSha).toBe(
|
|
458
|
+
'7'.repeat(40)
|
|
459
|
+
)
|
|
460
|
+
await writeManifest('8'.repeat(40), 'https://github.com/acme/replacement-plugin')
|
|
461
|
+
const replaced = await synchronize({ ...options, existingLock: refreshed.lock! })
|
|
462
|
+
expect(replaced.lock?.sources[0]?.pluginPins[0]?.repositoryUrl).toBe(
|
|
463
|
+
'https://github.com/acme/replacement-plugin'
|
|
464
|
+
)
|
|
465
|
+
expect(replaced.lock?.sources[0]?.pluginPins[0]?.resolvedCommitSha).toBe('8'.repeat(40))
|
|
466
|
+
} finally {
|
|
467
|
+
await rm(fixtureRoot, { recursive: true, force: true })
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test.skipIf(process.platform === 'win32')(
|
|
472
|
+
'pins annotated and lightweight tags to the target commit',
|
|
473
|
+
async () => {
|
|
474
|
+
const root = await mkdtemp(join(tmpdir(), 'plugin-tag-pin-'))
|
|
475
|
+
const repo = join(root, 'repository')
|
|
476
|
+
const bin = join(root, 'bin')
|
|
477
|
+
const originalPath = process.env.PATH
|
|
478
|
+
const originalGit = process.env.ADEA_TEST_REAL_GIT
|
|
479
|
+
const originalRepo = process.env.ADEA_TEST_TAG_REPO
|
|
480
|
+
const originalFetch = globalThis.fetch
|
|
481
|
+
const realGit = execFileSync('which', ['git'], { encoding: 'utf8' }).trim()
|
|
482
|
+
const git = (...args: string[]) =>
|
|
483
|
+
execFileSync(realGit, ['-C', repo, ...args], { encoding: 'utf8' }).trim()
|
|
484
|
+
try {
|
|
485
|
+
await mkdir(repo)
|
|
486
|
+
await mkdir(bin)
|
|
487
|
+
git('init', '-q', '-b', 'main')
|
|
488
|
+
git(
|
|
489
|
+
'-c',
|
|
490
|
+
'user.name=Fixture',
|
|
491
|
+
'-c',
|
|
492
|
+
'user.email=fixture@example.invalid',
|
|
493
|
+
'commit',
|
|
494
|
+
'--allow-empty',
|
|
495
|
+
'-qm',
|
|
496
|
+
'fixture'
|
|
497
|
+
)
|
|
498
|
+
git(
|
|
499
|
+
'-c',
|
|
500
|
+
'user.name=Fixture',
|
|
501
|
+
'-c',
|
|
502
|
+
'user.email=fixture@example.invalid',
|
|
503
|
+
'tag',
|
|
504
|
+
'-a',
|
|
505
|
+
'annotated',
|
|
506
|
+
'-m',
|
|
507
|
+
'fixture'
|
|
508
|
+
)
|
|
509
|
+
git('tag', 'lightweight')
|
|
510
|
+
const commit = git('rev-parse', 'HEAD')
|
|
511
|
+
expect(git('rev-parse', 'refs/tags/annotated')).not.toBe(commit)
|
|
512
|
+
const sourceSha = '5'.repeat(40)
|
|
513
|
+
const source: SourceConfig = {
|
|
514
|
+
...sources[0]!,
|
|
515
|
+
sourceId: 'tag-fixture',
|
|
516
|
+
repositoryUrl: 'https://github.com/acme/marketplace',
|
|
517
|
+
manifestPath: 'marketplace.json',
|
|
518
|
+
}
|
|
519
|
+
const script = join(bin, 'git')
|
|
520
|
+
await writeFile(
|
|
521
|
+
script,
|
|
522
|
+
`#!/bin/sh\nif [ "$2" = "${source.repositoryUrl}" ]; then\n printf '%s\\t%s\\n' '${sourceSha}' "$3"\nelse\n shift 2\n exec "$ADEA_TEST_REAL_GIT" ls-remote "$ADEA_TEST_TAG_REPO" "$@"\nfi\n`
|
|
523
|
+
)
|
|
524
|
+
await chmod(script, 0o700)
|
|
525
|
+
process.env.ADEA_TEST_REAL_GIT = realGit
|
|
526
|
+
process.env.ADEA_TEST_TAG_REPO = repo
|
|
527
|
+
process.env.PATH = `${bin}:${originalPath ?? ''}`
|
|
528
|
+
for (const ref of ['refs/tags/annotated', 'refs/tags/lightweight', 'refs/heads/main']) {
|
|
529
|
+
const manifest = JSON.stringify({
|
|
530
|
+
plugins: [
|
|
531
|
+
{
|
|
532
|
+
name: 'remote',
|
|
533
|
+
source: { source: 'git', url: 'https://github.com/acme/remote-plugin', ref },
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
})
|
|
537
|
+
globalThis.fetch = (async (_url: unknown) => new Response(manifest)) as typeof fetch
|
|
538
|
+
const loaded: string[] = []
|
|
539
|
+
const result = await synchronize({
|
|
540
|
+
sources: [source],
|
|
541
|
+
categoryMap,
|
|
542
|
+
productAliases,
|
|
543
|
+
policy,
|
|
544
|
+
mode: 'live',
|
|
545
|
+
snapshotLoader: {
|
|
546
|
+
load: async (_url, sha) => {
|
|
547
|
+
loaded.push(sha)
|
|
548
|
+
return {
|
|
549
|
+
files: new Map([['plugin.json', new TextEncoder().encode('{"name":"Remote"}')]]),
|
|
550
|
+
symlinks: [],
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
expect(result.lock?.sources[0]?.pluginPins[0]?.resolvedCommitSha).toBe(commit)
|
|
556
|
+
expect(loaded).toEqual([commit])
|
|
557
|
+
}
|
|
558
|
+
} finally {
|
|
559
|
+
globalThis.fetch = originalFetch
|
|
560
|
+
if (originalPath === undefined) delete process.env.PATH
|
|
561
|
+
else process.env.PATH = originalPath
|
|
562
|
+
if (originalGit === undefined) delete process.env.ADEA_TEST_REAL_GIT
|
|
563
|
+
else process.env.ADEA_TEST_REAL_GIT = originalGit
|
|
564
|
+
if (originalRepo === undefined) delete process.env.ADEA_TEST_TAG_REPO
|
|
565
|
+
else process.env.ADEA_TEST_TAG_REPO = originalRepo
|
|
566
|
+
await rm(root, { recursive: true, force: true })
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
test.skipIf(process.platform === 'win32')(
|
|
572
|
+
'refreshes floating external refs even when the marketplace head is unchanged',
|
|
573
|
+
async () => {
|
|
574
|
+
const bin = await mkdtemp(join(tmpdir(), 'plugin-live-pin-'))
|
|
575
|
+
const originalPath = process.env.PATH
|
|
576
|
+
const originalFetch = globalThis.fetch
|
|
577
|
+
const sourceSha = '5'.repeat(40)
|
|
578
|
+
const source: SourceConfig = {
|
|
579
|
+
...sources[0]!,
|
|
580
|
+
sourceId: 'live-pin-test',
|
|
581
|
+
repositoryUrl: 'https://github.com/acme/marketplace',
|
|
582
|
+
manifestPath: 'marketplace.json',
|
|
583
|
+
}
|
|
584
|
+
const manifest = JSON.stringify({
|
|
585
|
+
plugins: [
|
|
586
|
+
{
|
|
587
|
+
name: 'remote',
|
|
588
|
+
description: 'Remote plugin.',
|
|
589
|
+
source: { source: 'git', url: 'https://github.com/acme/remote-plugin', ref: 'main' },
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
})
|
|
593
|
+
const gitPath = join(bin, 'git')
|
|
594
|
+
const writeGit = async (pluginSha: string) => {
|
|
595
|
+
await writeFile(
|
|
596
|
+
gitPath,
|
|
597
|
+
`#!/bin/sh\nif [ "$2" = "${source.repositoryUrl}" ]; then\n printf '%s\\t%s\\n' '${sourceSha}' "$3"\nelse\n printf '%s\\t%s\\n' '${pluginSha}' "$3"\nfi\n`
|
|
598
|
+
)
|
|
599
|
+
await chmod(gitPath, 0o700)
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
process.env.PATH = `${bin}:${originalPath ?? ''}`
|
|
603
|
+
globalThis.fetch = (async (url) => {
|
|
604
|
+
expect(String(url)).toBe(
|
|
605
|
+
`https://raw.githubusercontent.com/acme/marketplace/${sourceSha}/marketplace.json`
|
|
606
|
+
)
|
|
607
|
+
return new Response(manifest)
|
|
608
|
+
}) as typeof fetch
|
|
609
|
+
const options = {
|
|
610
|
+
sources: [source],
|
|
611
|
+
categoryMap,
|
|
612
|
+
productAliases,
|
|
613
|
+
policy,
|
|
614
|
+
mode: 'live' as const,
|
|
615
|
+
snapshotLoader: {
|
|
616
|
+
load: async () => ({
|
|
617
|
+
files: new Map([['plugin.json', new TextEncoder().encode('{"name":"Remote"}')]]),
|
|
618
|
+
symlinks: [],
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
await writeGit('6'.repeat(40))
|
|
623
|
+
const first = await synchronize(options)
|
|
624
|
+
await writeGit('7'.repeat(40))
|
|
625
|
+
const next = await synchronize({ ...options, existingLock: first.lock! })
|
|
626
|
+
expect(next.changed).toBe(true)
|
|
627
|
+
expect(next.lock?.sources[0]?.pluginPins[0]?.resolvedCommitSha).toBe('7'.repeat(40))
|
|
628
|
+
const replay = await synchronize({ ...options, fromLock: first.lock! })
|
|
629
|
+
expect(replay.lock?.sources[0]?.pluginPins[0]?.resolvedCommitSha).toBe('6'.repeat(40))
|
|
630
|
+
} finally {
|
|
631
|
+
globalThis.fetch = originalFetch
|
|
632
|
+
if (originalPath === undefined) delete process.env.PATH
|
|
633
|
+
else process.env.PATH = originalPath
|
|
634
|
+
await rm(bin, { recursive: true, force: true })
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
|
|
412
639
|
test('skips apollo-skills symlinks deterministically without dropping safe plugins', async () => {
|
|
413
640
|
const fixtureRoot = await mkdtemp(join(tmpdir(), 'apollo-symlink-'))
|
|
414
641
|
await mkdir(join(fixtureRoot, 'apollo'), { recursive: true })
|
|
@@ -477,6 +704,26 @@ describe('deterministic catalog synchronization', () => {
|
|
|
477
704
|
verifyArtifacts(result.artifacts!)
|
|
478
705
|
})
|
|
479
706
|
|
|
707
|
+
test('preserves an existing recovery directory when writing artifacts', async () => {
|
|
708
|
+
const parent = await mkdtemp(join(tmpdir(), 'plugin-recovery-'))
|
|
709
|
+
const output = join(parent, 'generated')
|
|
710
|
+
const backup = `${output}.previous`
|
|
711
|
+
try {
|
|
712
|
+
await mkdir(backup)
|
|
713
|
+
await writeFile(join(backup, 'keep.txt'), 'recover me')
|
|
714
|
+
const artifacts = { 'catalog.v1.json': '{}' } as unknown as GeneratedArtifacts
|
|
715
|
+
await expect(writeArtifacts(output, artifacts)).rejects.toThrow('CATALOG_RECOVERY_REQUIRED')
|
|
716
|
+
expect(await readFile(join(backup, 'keep.txt'), 'utf8')).toBe('recover me')
|
|
717
|
+
await mkdir(output)
|
|
718
|
+
await writeFile(join(output, 'current.txt'), 'current')
|
|
719
|
+
await expect(writeArtifacts(output, artifacts)).rejects.toThrow('CATALOG_RECOVERY_REQUIRED')
|
|
720
|
+
expect(await readFile(join(backup, 'keep.txt'), 'utf8')).toBe('recover me')
|
|
721
|
+
expect(await readFile(join(output, 'current.txt'), 'utf8')).toBe('current')
|
|
722
|
+
} finally {
|
|
723
|
+
await rm(parent, { recursive: true, force: true })
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
|
|
480
727
|
test('keeps a failed build from producing a new artifact set', async () => {
|
|
481
728
|
const first = await synchronize({
|
|
482
729
|
sources,
|
|
@@ -537,37 +784,6 @@ describe('identity, classification, and harness contracts', () => {
|
|
|
537
784
|
)
|
|
538
785
|
expect(normalizeCategory('Developer Tools', categoryMap)).toBe('developer-tools')
|
|
539
786
|
})
|
|
540
|
-
|
|
541
|
-
test('generates a deterministic non-installing plan for every harness', async () => {
|
|
542
|
-
const result = await synchronize({
|
|
543
|
-
sources,
|
|
544
|
-
categoryMap,
|
|
545
|
-
productAliases,
|
|
546
|
-
policy,
|
|
547
|
-
mode: 'offline',
|
|
548
|
-
fixtureRoot: join(process.cwd(), 'fixtures'),
|
|
549
|
-
})
|
|
550
|
-
const catalog = result.catalog!
|
|
551
|
-
for (const harness of supportedHarnesses()) {
|
|
552
|
-
const plan = createMaterializationPlan({
|
|
553
|
-
catalog,
|
|
554
|
-
pluginId: 'plugin:openai-official:linear',
|
|
555
|
-
harness,
|
|
556
|
-
})
|
|
557
|
-
expect(plan.pluginId).toBe('plugin:openai-official:linear')
|
|
558
|
-
expect(plan.configuration.executeInstallation).toBe(false)
|
|
559
|
-
expect(JSON.stringify(plan)).toBe(
|
|
560
|
-
JSON.stringify(
|
|
561
|
-
createMaterializationPlan({
|
|
562
|
-
catalog,
|
|
563
|
-
pluginId: plan.pluginId,
|
|
564
|
-
harness,
|
|
565
|
-
releaseId: plan.releaseId,
|
|
566
|
-
})
|
|
567
|
-
)
|
|
568
|
-
)
|
|
569
|
-
}
|
|
570
|
-
})
|
|
571
787
|
})
|
|
572
788
|
|
|
573
789
|
describe('filesystem safety', () => {
|
|
@@ -699,9 +915,13 @@ describe('upstream fetch quarantine', () => {
|
|
|
699
915
|
})
|
|
700
916
|
|
|
701
917
|
test('quarantines a plugin when the archive keeps failing', async () => {
|
|
702
|
-
|
|
918
|
+
let recovered = false
|
|
919
|
+
const stub = stubFetch(async (url) => {
|
|
703
920
|
if (url === treeUrl) return jsonResponse(treePayload(['plugins/demo/plugin.json']))
|
|
704
|
-
if (isArchive(url))
|
|
921
|
+
if (isArchive(url))
|
|
922
|
+
return recovered
|
|
923
|
+
? tarballResponse([{ path: 'plugins/demo/plugin.json', body: manifestBytes }])
|
|
924
|
+
: new Response('broken', { status: 500 })
|
|
705
925
|
return new Response('unexpected', { status: 200 })
|
|
706
926
|
})
|
|
707
927
|
try {
|
|
@@ -712,6 +932,9 @@ describe('upstream fetch quarantine', () => {
|
|
|
712
932
|
)
|
|
713
933
|
expect(error?.message).toMatch(/^PLUGIN_SOURCE_UNAVAILABLE:/)
|
|
714
934
|
expect(stub.calls.filter(isArchive)).toHaveLength(3)
|
|
935
|
+
recovered = true
|
|
936
|
+
expect((await loader.load(repo, sha, 'plugins/demo')).files.has('plugin.json')).toBe(true)
|
|
937
|
+
expect(stub.calls.filter(isArchive)).toHaveLength(4)
|
|
715
938
|
} finally {
|
|
716
939
|
stub.restore()
|
|
717
940
|
}
|
|
@@ -745,11 +968,15 @@ describe('upstream fetch quarantine', () => {
|
|
|
745
968
|
|
|
746
969
|
test('does not retry a forbidden tree without rate-limit signals', async () => {
|
|
747
970
|
let treeCalls = 0
|
|
748
|
-
const stub = stubFetch((url) => {
|
|
971
|
+
const stub = stubFetch(async (url) => {
|
|
749
972
|
if (url === treeUrl) {
|
|
750
973
|
treeCalls += 1
|
|
751
|
-
return
|
|
974
|
+
return treeCalls === 1
|
|
975
|
+
? new Response('denied', { status: 403 })
|
|
976
|
+
: jsonResponse(treePayload(['plugins/demo/plugin.json']))
|
|
752
977
|
}
|
|
978
|
+
if (isArchive(url))
|
|
979
|
+
return tarballResponse([{ path: 'plugins/demo/plugin.json', body: manifestBytes }])
|
|
753
980
|
return new Response('unexpected', { status: 500 })
|
|
754
981
|
})
|
|
755
982
|
try {
|
|
@@ -760,6 +987,8 @@ describe('upstream fetch quarantine', () => {
|
|
|
760
987
|
)
|
|
761
988
|
expect(error?.message).toMatch(/^PLUGIN_SOURCE_UNAVAILABLE:/)
|
|
762
989
|
expect(treeCalls).toBe(1)
|
|
990
|
+
expect((await loader.load(repo, sha, 'plugins/demo')).files.has('plugin.json')).toBe(true)
|
|
991
|
+
expect(treeCalls).toBe(2)
|
|
763
992
|
} finally {
|
|
764
993
|
stub.restore()
|
|
765
994
|
}
|
|
@@ -779,11 +1008,14 @@ describe('upstream fetch quarantine', () => {
|
|
|
779
1008
|
})
|
|
780
1009
|
try {
|
|
781
1010
|
const loader = new NetworkSnapshotLoader(policy)
|
|
782
|
-
const first = await
|
|
783
|
-
|
|
1011
|
+
const [first, second] = await Promise.all([
|
|
1012
|
+
loader.load(repo, sha, 'plugins/one'),
|
|
1013
|
+
loader.load(repo, sha, 'plugins/two'),
|
|
1014
|
+
])
|
|
784
1015
|
expect([...first.files.keys()]).toEqual(['plugin.json'])
|
|
785
1016
|
expect([...second.files.keys()].toSorted()).toEqual(['plugin.json', 'run.sh'])
|
|
786
|
-
//
|
|
1017
|
+
// Concurrent plugins share both requests, not only the finished cache.
|
|
1018
|
+
expect(stub.calls.filter((url) => url === treeUrl)).toHaveLength(1)
|
|
787
1019
|
expect(stub.calls.filter(isArchive)).toHaveLength(1)
|
|
788
1020
|
expect(stub.calls.filter(isRawGitHubHost)).toHaveLength(0)
|
|
789
1021
|
} finally {
|