@danypops/pi-packed 0.19.11 → 0.20.0
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/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/protocol.d.ts +0 -1
- package/dist/protocol.d.ts.map +1 -1
- package/extension/src/model.ts +11 -4
- package/package.json +17 -5
- package/service/src/adoption/check.ts +39 -4
- package/service/src/adoption/doctor.ts +21 -1
- package/service/src/adoption/service-doctor.ts +101 -0
- package/service/src/cli/cli.ts +32 -60
- package/service/src/daemon/client.ts +20 -26
- package/service/src/daemon/daemon-service.ts +127 -37
- package/service/src/daemon/daemon.ts +24 -2
- package/service/src/daemon/service.ts +76 -11
- package/service/src/daemon/vehicle-registration.ts +173 -0
- package/service/src/public/client.ts +7 -19
- package/service/src/public/protocol.ts +0 -1
- package/service/src/security/security.ts +2 -0
- package/service/src/shared/constants.ts +2 -0
- package/service/test/check.test.ts +76 -0
- package/service/test/cli.test.ts +123 -138
- package/service/test/daemon-kit-migration.test.ts +1 -0
- package/service/test/daemon-maintenance.test.ts +127 -0
- package/service/test/daemon-service.test.ts +93 -40
- package/service/test/public-client.test.ts +1 -1
- package/service/test/security.test.ts +1 -0
- package/service/test/service-doctor.test.ts +100 -0
- package/service/test/service.test.ts +89 -13
- package/service/test/vehicle-registration.test.ts +121 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packed's full 29-operation daemon surface projected onto the real Vehicle
|
|
3
|
+
* protocol. Every operation delegates to the exact same executeOperation()
|
|
4
|
+
* function /api/v1/ops already calls (one implementation, two projections --
|
|
5
|
+
* the same shape every other Vehicle-migrated daemon in this ecosystem
|
|
6
|
+
* uses) -- no behavior change, only a second real transport served
|
|
7
|
+
* alongside (not replacing) the existing /api/v1/ops route.
|
|
8
|
+
*
|
|
9
|
+
* Every operation's input is already a well-typed OperationInputs[Name] that
|
|
10
|
+
* executeOperation() validates and dispatches internally (throwing
|
|
11
|
+
* PackageOperationError on a bad shape/denied approval) -- there is no
|
|
12
|
+
* separate Vehicle-side schema to duplicate that logic, so both input and
|
|
13
|
+
* output use passthroughVehicleSchema and let executeOperation's own
|
|
14
|
+
* validation (already covered by service.test.ts) be the single source of
|
|
15
|
+
* truth for what's accepted.
|
|
16
|
+
*
|
|
17
|
+
* Effect classification is grounded in security.ts's own PACKAGE_OPERATIONS
|
|
18
|
+
* classification (read/maintenance/code-execution/settings-mutation/
|
|
19
|
+
* security-mutation) -- the codebase's own, already-deployed risk model --
|
|
20
|
+
* translated to Vehicle's effect vocabulary rather than independently
|
|
21
|
+
* re-derived, with two deliberate refinements documented at each entry
|
|
22
|
+
* where Vehicle's own taxonomy draws a finer distinction Packed's doesn't
|
|
23
|
+
* (package.remove -> destructive, since Vehicle has a dedicated category
|
|
24
|
+
* for irreversible deletion; restart_service/reconcile_services ->
|
|
25
|
+
* external-write rather than open-world, since restarting/reconciling an
|
|
26
|
+
* ALREADY-installed, already-vetted service introduces no new code, unlike
|
|
27
|
+
* install/install_service/update/setup.apply which can fetch and run
|
|
28
|
+
* arbitrary newly-published code).
|
|
29
|
+
*
|
|
30
|
+
* Approval/authorization is NOT reimplemented here: executeOperation()
|
|
31
|
+
* (and the route()-based operations it delegates to internally) already
|
|
32
|
+
* calls authorize()/assertPackagePermission() and throws
|
|
33
|
+
* PackageOperationError(status: 403) on a denied mutation -- reusing
|
|
34
|
+
* executeOperation() verbatim means this Vehicle surface automatically
|
|
35
|
+
* inherits the exact same approval gate, with zero duplicated policy logic
|
|
36
|
+
* to drift out of sync.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { VehicleEffect, VehicleIdempotency } from "@danypops/vehicle-core";
|
|
40
|
+
import { bindVehicleOperation, defineVehicleOperation, passthroughVehicleSchema, VehicleError } from "@danypops/vehicle-core";
|
|
41
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
42
|
+
import { OPERATION_NAMES, type OperationInputs, type OperationName, type OperationOutputs } from "./service.ts";
|
|
43
|
+
|
|
44
|
+
/** Thrown by executeOperation() with an HTTP-shaped .status; preserves /api/v1/ops's own status->meaning exactly instead of letting VehicleRegistry.invoke()'s catch-all flatten every failure into a generic internal/500 with the message discarded. */
|
|
45
|
+
interface StatusCarryingError extends Error {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasStatus(error: unknown): error is StatusCarryingError {
|
|
50
|
+
return error instanceof Error && typeof (error as { status?: unknown }).status === "number";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function withPackedErrorParity<T>(run: () => T | Promise<T>): Promise<T> {
|
|
54
|
+
try {
|
|
55
|
+
return await run();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof VehicleError) throw error;
|
|
58
|
+
if (hasStatus(error)) {
|
|
59
|
+
const category =
|
|
60
|
+
error.status === 403
|
|
61
|
+
? "authorization"
|
|
62
|
+
: error.status === 404
|
|
63
|
+
? "not_found"
|
|
64
|
+
: error.status === 400
|
|
65
|
+
? "validation"
|
|
66
|
+
: error.status >= 500
|
|
67
|
+
? "unavailable"
|
|
68
|
+
: "validation";
|
|
69
|
+
throw new VehicleError("operation-rejected", error.message, { category, cause: error });
|
|
70
|
+
}
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new VehicleError("operation-rejected", message, { category: "validation", cause: error });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const OWNER = "packed";
|
|
77
|
+
const LIMITS = { defaultTimeoutMs: 30_000, maxTimeoutMs: 120_000, maxRequestBytes: 65_536, maxResponseBytes: 4_194_304 };
|
|
78
|
+
|
|
79
|
+
const READ: VehicleIdempotency = { mode: "safe" };
|
|
80
|
+
const WRITE: VehicleIdempotency = { mode: "unsafe" };
|
|
81
|
+
|
|
82
|
+
interface OperationMeta {
|
|
83
|
+
readonly description: string;
|
|
84
|
+
readonly effect: VehicleEffect;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One entry per OPERATION_NAMES member. See this file's own doc comment for
|
|
89
|
+
* the general translation rule (security.ts's PACKAGE_OPERATIONS
|
|
90
|
+
* classification) and its two deliberate refinements.
|
|
91
|
+
*/
|
|
92
|
+
const OPERATION_META: Record<OperationName, OperationMeta> = {
|
|
93
|
+
"package.search": { description: "Searches Pi packages on npm.", effect: "read" },
|
|
94
|
+
"package.info": { description: "Reads bounded metadata for one package.", effect: "read" },
|
|
95
|
+
"package.installed": { description: "Lists locally installed Pi packages.", effect: "read" },
|
|
96
|
+
"package.catalog": { description: "Reads the local SQLite catalog mirror.", effect: "read" },
|
|
97
|
+
"package.catalog.sync": {
|
|
98
|
+
description: "Refreshes the local catalog mirror from the Pi-package-tagged npm registry subset.",
|
|
99
|
+
effect: "external-write",
|
|
100
|
+
},
|
|
101
|
+
"package.index": { description: "Reads the locally built adoption-score index, if one exists.", effect: "read" },
|
|
102
|
+
"package.index.build": {
|
|
103
|
+
description: "Builds the adoption-score index by scoring catalog entries against the npm registry.",
|
|
104
|
+
effect: "external-write",
|
|
105
|
+
},
|
|
106
|
+
"package.updates": { description: "Reads the last background update-check snapshot.", effect: "read" },
|
|
107
|
+
"package.check": { description: "Runs static (and optionally smoke-test) quality checks against a local package path.", effect: "read" },
|
|
108
|
+
"package.pack": { description: "Verifies a local package path via npm pack.", effect: "read" },
|
|
109
|
+
"package.score": { description: "Computes an adoption-readiness score for a local path or registry package.", effect: "read" },
|
|
110
|
+
"setup.export": { description: "Exports the current Pi setup as a portable manifest.", effect: "local-write" },
|
|
111
|
+
"setup.update": { description: "Updates an existing setup manifest in place.", effect: "local-write" },
|
|
112
|
+
"setup.plan": { description: "Computes a setup manifest's install/update/remove plan without applying it.", effect: "read" },
|
|
113
|
+
"setup.apply": {
|
|
114
|
+
description: "Applies a setup manifest's plan -- can install, update, or remove packages.",
|
|
115
|
+
effect: "open-world",
|
|
116
|
+
},
|
|
117
|
+
"package.security.get": { description: "Reads this daemon's mutation-approval security settings.", effect: "read" },
|
|
118
|
+
"package.security.set": { description: "Writes this daemon's mutation-approval security settings.", effect: "local-write" },
|
|
119
|
+
"package.install": { description: "Installs a Pi package from an npm, git, or https source.", effect: "open-world" },
|
|
120
|
+
"package.install_service": {
|
|
121
|
+
description: "Installs a persistent supervised service for an already-installed daemon package.",
|
|
122
|
+
effect: "open-world",
|
|
123
|
+
},
|
|
124
|
+
"package.restart_service": {
|
|
125
|
+
description: "Restarts an already-installed package's persistent service -- no new code introduced.",
|
|
126
|
+
effect: "external-write",
|
|
127
|
+
},
|
|
128
|
+
"package.reconcile_services": {
|
|
129
|
+
description: "Reconciles every installed daemon package's persistent service against desired state.",
|
|
130
|
+
effect: "external-write",
|
|
131
|
+
},
|
|
132
|
+
"package.remove": { description: "Removes an installed Pi package. Irreversible.", effect: "destructive" },
|
|
133
|
+
"package.update": { description: "Updates a configured Pi package to its latest available version.", effect: "open-world" },
|
|
134
|
+
"resources.list": { description: "Lists global and project-scoped Pi resources (extensions, skills, prompts, themes).", effect: "read" },
|
|
135
|
+
"resources.toggle": { description: "Enables or disables one Pi resource in a settings file.", effect: "local-write" },
|
|
136
|
+
"pi.status": { description: "Reports the locally running Pi version against the latest published release.", effect: "read" },
|
|
137
|
+
"advisories.scan": { description: "Scans installed package versions against known advisories.", effect: "read" },
|
|
138
|
+
"doctor.run": { description: "Runs diagnostic health checks (service install drift, resource config, ...).", effect: "read" },
|
|
139
|
+
"package.updates.project": { description: "Checks for updates across every scope visible to one project.", effect: "read" },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Read effects need only packed:read; every other effect needs both (writes commonly also read first). */
|
|
143
|
+
function permissionsFor(effect: VehicleEffect): readonly string[] {
|
|
144
|
+
return effect === "read" ? ["packed:read"] : ["packed:read", "packed:write"];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function registerPackedVehicleOperations(
|
|
148
|
+
registry: VehicleRegistry,
|
|
149
|
+
executeOperation: <Name extends OperationName>(op: Name, input: OperationInputs[Name]) => Promise<OperationOutputs[Name]>,
|
|
150
|
+
): void {
|
|
151
|
+
for (const name of OPERATION_NAMES) {
|
|
152
|
+
const meta = OPERATION_META[name];
|
|
153
|
+
const operation = defineVehicleOperation({
|
|
154
|
+
name,
|
|
155
|
+
version: 1,
|
|
156
|
+
description: meta.description,
|
|
157
|
+
input: passthroughVehicleSchema,
|
|
158
|
+
output: passthroughVehicleSchema,
|
|
159
|
+
permissions: permissionsFor(meta.effect),
|
|
160
|
+
effect: meta.effect,
|
|
161
|
+
idempotency: meta.effect === "read" ? READ : WRITE,
|
|
162
|
+
limits: LIMITS,
|
|
163
|
+
});
|
|
164
|
+
registry.register(
|
|
165
|
+
OWNER,
|
|
166
|
+
bindVehicleOperation(
|
|
167
|
+
operation,
|
|
168
|
+
() => async (context) =>
|
|
169
|
+
withPackedErrorParity(() => executeOperation(name, context.input as OperationInputs[typeof name])),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
|
|
6
6
|
import { readDaemonHandle, resolveDaemonPaths } from "@danypops/vehicle-server/paths";
|
|
7
|
-
import { isServiceInstalled as vehicleIsServiceInstalled } from "@danypops/vehicle-server/service";
|
|
7
|
+
import { createNodeServiceInstallDeps, isServiceInstalled as vehicleIsServiceInstalled } from "@danypops/vehicle-server/service";
|
|
8
8
|
import type {
|
|
9
9
|
ExtensionOperationInputs,
|
|
10
10
|
ExtensionOperationName,
|
|
@@ -89,21 +89,9 @@ export function resolvePackedClientPaths(options: PackedPathOptions = {}): Packe
|
|
|
89
89
|
return { token: paths.token, handle: paths.handle, serviceDescriptor: paths.serviceDescriptor };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return vehicleIsServiceInstalled(
|
|
96
|
-
{ name: SERVICE_NAME, binPath: "", descriptorPath: serviceDescriptor },
|
|
97
|
-
{
|
|
98
|
-
fileExists: existsSync,
|
|
99
|
-
writeFile: () => {},
|
|
100
|
-
readFile: () => null,
|
|
101
|
-
removeFile: () => {},
|
|
102
|
-
mkdirp: () => {},
|
|
103
|
-
runCommand: () => ({ ok: false, output: "" }),
|
|
104
|
-
which: () => false,
|
|
105
|
-
},
|
|
106
|
-
);
|
|
92
|
+
/** Checks Armada's authoritative desired fleet rather than native descriptor files. */
|
|
93
|
+
function isPackedServiceInstalled(): boolean {
|
|
94
|
+
return vehicleIsServiceInstalled(SERVICE_NAME, createNodeServiceInstallDeps());
|
|
107
95
|
}
|
|
108
96
|
|
|
109
97
|
export type FetchTransport = (request: Request) => Promise<Response>;
|
|
@@ -251,7 +239,7 @@ export async function ensureClient(deps: EnsureClientDeps): Promise<PackedClient
|
|
|
251
239
|
const waitedSeconds = (attempts * delayMs) / 1000;
|
|
252
240
|
throw new Error(
|
|
253
241
|
serviceInstalled
|
|
254
|
-
? `Packed daemon did not become ready within ${waitedSeconds} seconds, but a
|
|
242
|
+
? `Packed daemon did not become ready within ${waitedSeconds} seconds, but a managed service is installed -- run packed doctor rather than auto-spawning a second one`
|
|
255
243
|
: `Packed daemon did not become ready within ${waitedSeconds} seconds`,
|
|
256
244
|
);
|
|
257
245
|
}
|
|
@@ -259,7 +247,7 @@ export async function ensureClient(deps: EnsureClientDeps): Promise<PackedClient
|
|
|
259
247
|
export async function ensurePackedClient(paths = resolvePackedClientPaths(), transport: FetchTransport = fetch): Promise<PackedClient> {
|
|
260
248
|
return ensureClient({
|
|
261
249
|
connect: () => connectPackedClient(paths, transport),
|
|
262
|
-
isServiceInstalled: () => isPackedServiceInstalled(
|
|
250
|
+
isServiceInstalled: () => isPackedServiceInstalled(),
|
|
263
251
|
spawn: () => {
|
|
264
252
|
const override = process.env.PI_PACKED_BIN;
|
|
265
253
|
const command = override ?? process.env.PI_PACKED_BUN ?? "bun";
|
|
@@ -92,7 +92,6 @@ export type PackageResources = { source: string; name: string; scope: "global" |
|
|
|
92
92
|
export interface ServiceSpecSummary {
|
|
93
93
|
name: string;
|
|
94
94
|
binPath: string;
|
|
95
|
-
descriptorPath: string;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
/** Mirrors service/src/pi/pi-version.ts's CURRENT_PI_PACKAGE_NAME
|
|
@@ -23,6 +23,7 @@ export const PACKAGE_OPERATIONS = [
|
|
|
23
23
|
"install",
|
|
24
24
|
"install_service",
|
|
25
25
|
"restart_service",
|
|
26
|
+
"reconcile_services",
|
|
26
27
|
"setup.apply",
|
|
27
28
|
"update",
|
|
28
29
|
"update.self",
|
|
@@ -70,6 +71,7 @@ const CLASSIFICATIONS: Record<PackageOperation, PackageOperationClassification>
|
|
|
70
71
|
install: "code-execution",
|
|
71
72
|
install_service: "code-execution",
|
|
72
73
|
restart_service: "code-execution",
|
|
74
|
+
reconcile_services: "code-execution",
|
|
73
75
|
"setup.apply": "code-execution",
|
|
74
76
|
update: "code-execution",
|
|
75
77
|
"update.self": "code-execution",
|
|
@@ -52,6 +52,7 @@ export const INDEX_OPERATION_TIMEOUT_MS = 10 * 60_000;
|
|
|
52
52
|
export const WATCH_INTERVAL_DEFAULT_MS = 30 * 60_000; // updates diff cadence
|
|
53
53
|
export const CATALOG_INTERVAL_DEFAULT_MS = 6 * 3_600_000; // full mirror TTL
|
|
54
54
|
export const INDEX_INTERVAL_DEFAULT_MS = 6 * 3_600_000; // static index regeneration TTL, same cadence as the catalog mirror it reads from
|
|
55
|
+
export const RECONCILE_INTERVAL_DEFAULT_MS = 30 * 60_000; // Vehicle-service drift sweep cadence -- catches an out-of-band npm install/update a running daemon never picked up
|
|
55
56
|
export const IDLE_BUDGET_DEFAULT_MS = 10 * 60_000; // on-demand self-exit
|
|
56
57
|
export const WATCHDOG_TICK_MS = 15_000;
|
|
57
58
|
|
|
@@ -69,5 +70,6 @@ export const ENV = {
|
|
|
69
70
|
WATCH_SECS: "PI_PACKED_WATCH_SECS",
|
|
70
71
|
CATALOG_SECS: "PI_PACKED_CATALOG_SECS",
|
|
71
72
|
INDEX_SECS: "PI_PACKED_INDEX_SECS",
|
|
73
|
+
RECONCILE_SECS: "PI_PACKED_RECONCILE_SECS",
|
|
72
74
|
IDLE_SECS: "PI_PACKED_IDLE_SECS",
|
|
73
75
|
} as const;
|
|
@@ -366,3 +366,79 @@ describe("packed check", () => {
|
|
|
366
366
|
expect(codes(report.diagnostics)).toEqual(["PKG_JSON_INVALID"]);
|
|
367
367
|
});
|
|
368
368
|
});
|
|
369
|
+
|
|
370
|
+
describe("dependencyCheck false-positive regressions (packed check)", () => {
|
|
371
|
+
it("never scans a test file's own fixture strings as if they were real imports of that file", async () => {
|
|
372
|
+
// A checker's own test suite legitimately contains string literals shaped exactly
|
|
373
|
+
// like real import statements (fixture data for testing detection itself) -- these
|
|
374
|
+
// must never be attributed to the test file that merely contains them as text.
|
|
375
|
+
const root = fixture(
|
|
376
|
+
{ ...base, files: ["extensions", "test", "README.md", "LICENSE"] },
|
|
377
|
+
{
|
|
378
|
+
"extensions/index.ts": "export default function () {}",
|
|
379
|
+
"test/check.test.ts": 'const fixtureSource = \'import lodash from "lodash";\';\n',
|
|
380
|
+
"README.md": "# Example",
|
|
381
|
+
LICENSE: "MIT",
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
const report = await checkPackage(root, { generic: false });
|
|
385
|
+
expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("never scans a test-directory file outside a *.test.ts basename either (e.g. a shared test helper)", async () => {
|
|
389
|
+
const root = fixture(
|
|
390
|
+
{ ...base, files: ["extensions", "test", "README.md", "LICENSE"] },
|
|
391
|
+
{
|
|
392
|
+
"extensions/index.ts": "export default function () {}",
|
|
393
|
+
"test/helper.ts": 'import leftpad from "leftpad";\n',
|
|
394
|
+
"README.md": "# Example",
|
|
395
|
+
LICENSE: "MIT",
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
const report = await checkPackage(root, { generic: false });
|
|
399
|
+
expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("never treats a quoted 'from ...' phrase inside a comment as a real import specifier", async () => {
|
|
403
|
+
const root = fixture(
|
|
404
|
+
{ ...base, files: ["extensions", "README.md", "LICENSE"] },
|
|
405
|
+
{
|
|
406
|
+
"extensions/index.ts":
|
|
407
|
+
'/**\n * migrated "latest" from "some-package that looks like an import"\n */\nexport default function () {}\n',
|
|
408
|
+
"README.md": "# Example",
|
|
409
|
+
LICENSE: "MIT",
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
const report = await checkPackage(root, { generic: false });
|
|
413
|
+
expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("still catches a real RUNTIME_DEPENDENCY_MISSING import sitting right next to a misleading comment", async () => {
|
|
417
|
+
const root = fixture(
|
|
418
|
+
{ ...base, files: ["extensions", "README.md", "LICENSE"] },
|
|
419
|
+
{
|
|
420
|
+
"extensions/index.ts":
|
|
421
|
+
'// migrated "latest" from "totally not a real package"\nimport real from "real-missing-dep";\nexport default function () { return real; }\n',
|
|
422
|
+
"README.md": "# Example",
|
|
423
|
+
LICENSE: "MIT",
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
const report = await checkPackage(root, { generic: false });
|
|
427
|
+
expect(codes(report.diagnostics)).toContain("RUNTIME_DEPENDENCY_MISSING");
|
|
428
|
+
const diagnostic = report.diagnostics.find((d) => d.code === "RUNTIME_DEPENDENCY_MISSING")!;
|
|
429
|
+
expect(diagnostic.message).toContain("real-missing-dep");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("never flags a package's own self-import of its own subpath export as a missing runtime dependency", async () => {
|
|
433
|
+
const root = fixture(
|
|
434
|
+
{ ...base, name: "@scope/self-ref", files: ["extensions", "README.md", "LICENSE"] },
|
|
435
|
+
{
|
|
436
|
+
"extensions/index.ts": 'import { thing } from "@scope/self-ref/client";\nexport default function () { return thing; }\n',
|
|
437
|
+
"README.md": "# Example",
|
|
438
|
+
LICENSE: "MIT",
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
const report = await checkPackage(root, { generic: false });
|
|
442
|
+
expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
|
|
443
|
+
});
|
|
444
|
+
});
|