@elench/testkit 0.1.144 → 0.1.145
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 +10 -0
- package/lib/cli/commands/cleanup.mjs +12 -0
- package/lib/cli/operations/cleanup/operation.mjs +3 -0
- package/lib/cli/operations/destroy/operation.mjs +6 -1
- package/lib/database/index.mjs +163 -5
- package/lib/ownership/docker.mjs +135 -0
- package/lib/runner/maintenance.mjs +23 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ npx @elench/testkit --type int --write-status
|
|
|
61
61
|
npx @elench/testkit status
|
|
62
62
|
npx @elench/testkit destroy
|
|
63
63
|
npx @elench/testkit cleanup
|
|
64
|
+
npx @elench/testkit cleanup --resources --global --dry-run
|
|
64
65
|
|
|
65
66
|
# Local production environment
|
|
66
67
|
npx @elench/testkit local up
|
|
@@ -367,6 +368,15 @@ environment state. Testkit only supports local environments here; it does not
|
|
|
367
368
|
copy production data and it refuses managed runtime database URLs that are not
|
|
368
369
|
loopback PostgreSQL URLs.
|
|
369
370
|
|
|
371
|
+
Testkit-owned Docker resources are labelled at creation time so they can be
|
|
372
|
+
found again even if product-local `.testkit` state is deleted. `testkit destroy`
|
|
373
|
+
removes labelled resources for the current product after service state is
|
|
374
|
+
destroyed. `testkit cleanup --resources` cleans stale resources for the current
|
|
375
|
+
product; add `--global` to clean labelled resources whose product directory no
|
|
376
|
+
longer exists, and add `--include-legacy` only when you intentionally want to
|
|
377
|
+
remove old unlabelled `testkit_pg_*` containers from pre-ownership Testkit
|
|
378
|
+
versions. Use `--dry-run` first when cleaning globally.
|
|
379
|
+
|
|
370
380
|
`database.template` is the database-side equivalent for reusable template DB
|
|
371
381
|
state. When `database.sourceSchema` is configured, Testkit treats the configured
|
|
372
382
|
source database as the schema source of truth. A normal `testkit run` resolves a
|
|
@@ -19,6 +19,18 @@ export default class CleanupCommand extends Command {
|
|
|
19
19
|
multiple: true,
|
|
20
20
|
options: ["runtime", "bundles", "assistant", "all"],
|
|
21
21
|
}),
|
|
22
|
+
resources: Flags.boolean({
|
|
23
|
+
description: "Clean Testkit-owned Docker resources that are stale or orphaned",
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
26
|
+
global: Flags.boolean({
|
|
27
|
+
description: "Find orphaned Testkit-owned resources across all products",
|
|
28
|
+
default: false,
|
|
29
|
+
}),
|
|
30
|
+
"include-legacy": Flags.boolean({
|
|
31
|
+
description: "Include old unlabelled testkit_pg_* Docker containers in resource cleanup",
|
|
32
|
+
default: false,
|
|
33
|
+
}),
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
async run() {
|
|
@@ -9,5 +9,8 @@ export async function executeCleanupOperation(flags = {}) {
|
|
|
9
9
|
serviceName: flags.service || null,
|
|
10
10
|
dryRun: flags["dry-run"],
|
|
11
11
|
cache: flags.cache || [],
|
|
12
|
+
resources: flags.resources,
|
|
13
|
+
globalResources: flags.global,
|
|
14
|
+
includeLegacyResources: flags["include-legacy"],
|
|
12
15
|
});
|
|
13
16
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import * as runner from "../../../runner/index.mjs";
|
|
2
2
|
import { loadManagedConfigs } from "../../../app/configs.mjs";
|
|
3
|
+
import { destroyOwnedLocalDatabaseResources } from "../../../database/index.mjs";
|
|
3
4
|
|
|
4
5
|
export async function executeDestroyOperation(flags = {}) {
|
|
5
|
-
const { configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
6
|
+
const { allConfigs, configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
6
7
|
const results = [];
|
|
7
8
|
for (const config of configs) {
|
|
8
9
|
await runner.destroy(config);
|
|
9
10
|
results.push({ name: config.name, destroyed: true });
|
|
10
11
|
}
|
|
12
|
+
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
13
|
+
if (!flags.service) {
|
|
14
|
+
await destroyOwnedLocalDatabaseResources(productDir);
|
|
15
|
+
}
|
|
11
16
|
return results;
|
|
12
17
|
}
|
package/lib/database/index.mjs
CHANGED
|
@@ -29,6 +29,19 @@ import {
|
|
|
29
29
|
runTemplateStep,
|
|
30
30
|
} from "./template-steps.mjs";
|
|
31
31
|
import { materializeDatabaseConfig } from "../config/database-materialization.mjs";
|
|
32
|
+
import {
|
|
33
|
+
TESTKIT_PRODUCT_DIR_LABEL,
|
|
34
|
+
TESTKIT_PRODUCT_ID_LABEL,
|
|
35
|
+
TESTKIT_RESOURCE_KIND_LABEL,
|
|
36
|
+
TESTKIT_SCOPE_LABEL,
|
|
37
|
+
buildDockerResourceLabels,
|
|
38
|
+
buildProductIdentity,
|
|
39
|
+
dockerContainerSummary,
|
|
40
|
+
dockerLabelArgs,
|
|
41
|
+
listLegacyTestkitPostgresContainers,
|
|
42
|
+
listManagedDockerContainers,
|
|
43
|
+
removeDockerContainer,
|
|
44
|
+
} from "../ownership/docker.mjs";
|
|
32
45
|
import {
|
|
33
46
|
applySourceSchemaCache,
|
|
34
47
|
createSourceSchemaMismatchError,
|
|
@@ -130,15 +143,45 @@ export async function destroyServiceDatabaseCache(productDir, serviceName) {
|
|
|
130
143
|
|
|
131
144
|
export async function cleanupOrphanedLocalInfrastructure(productDir) {
|
|
132
145
|
const infraDir = getLocalInfraDir(productDir);
|
|
133
|
-
if (!fs.existsSync(infraDir)) return;
|
|
134
146
|
|
|
135
147
|
if (hasRemainingLocalArtifacts(productDir)) return;
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
if (fs.existsSync(infraDir)) {
|
|
150
|
+
const containerName = readStateValue(path.join(infraDir, "container_name"));
|
|
151
|
+
if (containerName) {
|
|
152
|
+
await stopAndRemoveContainer(containerName);
|
|
153
|
+
}
|
|
154
|
+
fs.rmSync(infraDir, { recursive: true, force: true });
|
|
140
155
|
}
|
|
141
|
-
|
|
156
|
+
|
|
157
|
+
await cleanupOwnedLocalDatabaseResources(productDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function destroyOwnedLocalDatabaseResources(productDir, options = {}) {
|
|
161
|
+
return cleanupOwnedLocalDatabaseResources(productDir, {
|
|
162
|
+
...options,
|
|
163
|
+
force: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function cleanupOwnedLocalDatabaseResources(productDir, options = {}) {
|
|
168
|
+
return cleanupLocalPostgresDockerResources({
|
|
169
|
+
productDir,
|
|
170
|
+
dryRun: Boolean(options.dryRun),
|
|
171
|
+
force: Boolean(options.force),
|
|
172
|
+
global: false,
|
|
173
|
+
includeLegacy: Boolean(options.includeLegacy),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function cleanupGlobalLocalDatabaseResources(options = {}) {
|
|
178
|
+
return cleanupLocalPostgresDockerResources({
|
|
179
|
+
productDir: options.productDir || process.cwd(),
|
|
180
|
+
dryRun: Boolean(options.dryRun),
|
|
181
|
+
force: false,
|
|
182
|
+
global: true,
|
|
183
|
+
includeLegacy: Boolean(options.includeLegacy),
|
|
184
|
+
});
|
|
142
185
|
}
|
|
143
186
|
|
|
144
187
|
export function isDatabaseStateDir(dir) {
|
|
@@ -472,11 +515,13 @@ async function ensureLocalContainer(productDir, database = {}) {
|
|
|
472
515
|
}
|
|
473
516
|
|
|
474
517
|
if (!inspect) {
|
|
518
|
+
const labels = buildLocalPostgresContainerLabels(productDir, containerName);
|
|
475
519
|
await execa("docker", [
|
|
476
520
|
"run",
|
|
477
521
|
"-d",
|
|
478
522
|
"--name",
|
|
479
523
|
containerName,
|
|
524
|
+
...dockerLabelArgs(labels),
|
|
480
525
|
"-e",
|
|
481
526
|
`POSTGRES_USER=${user}`,
|
|
482
527
|
"-e",
|
|
@@ -865,6 +910,117 @@ function parsePostgresConnectionUrl(rawUrl) {
|
|
|
865
910
|
};
|
|
866
911
|
}
|
|
867
912
|
|
|
913
|
+
async function cleanupLocalPostgresDockerResources(options = {}) {
|
|
914
|
+
const product = buildProductIdentity(options.productDir || process.cwd());
|
|
915
|
+
const managed = await listManagedDockerContainers();
|
|
916
|
+
const targets = [];
|
|
917
|
+
const kept = [];
|
|
918
|
+
|
|
919
|
+
for (const container of managed) {
|
|
920
|
+
if (!isManagedLocalPostgresContainer(container)) continue;
|
|
921
|
+
const classification = classifyManagedLocalPostgresContainer(container, product, options);
|
|
922
|
+
if (classification.remove) {
|
|
923
|
+
targets.push({ ...container, reason: classification.reason, legacy: false });
|
|
924
|
+
} else if (classification.reason) {
|
|
925
|
+
kept.push({ ...container, reason: classification.reason, legacy: false });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (options.includeLegacy) {
|
|
930
|
+
const currentLegacyName = buildContainerName(product.dir);
|
|
931
|
+
for (const container of await listLegacyTestkitPostgresContainers()) {
|
|
932
|
+
if (!options.global && container.name !== currentLegacyName) continue;
|
|
933
|
+
targets.push({ ...container, reason: "legacy-unlabelled", legacy: true });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!options.dryRun) {
|
|
938
|
+
for (const container of targets) {
|
|
939
|
+
await removeDockerContainer(container.name || container.id);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
removed: options.dryRun ? [] : targets,
|
|
945
|
+
targets,
|
|
946
|
+
kept,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function classifyManagedLocalPostgresContainer(container, product, options) {
|
|
951
|
+
const labels = container.labels || {};
|
|
952
|
+
const containerProductId = labels[TESTKIT_PRODUCT_ID_LABEL] || "";
|
|
953
|
+
const containerProductDir = labels[TESTKIT_PRODUCT_DIR_LABEL] || "";
|
|
954
|
+
const currentProduct = containerProductId === product.id;
|
|
955
|
+
|
|
956
|
+
if (options.force && currentProduct) {
|
|
957
|
+
return { remove: true, reason: "destroy-current-product" };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (!options.global && !currentProduct) {
|
|
961
|
+
return { remove: false, reason: "different-product" };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (currentProduct) {
|
|
965
|
+
if (!hasRemainingLocalArtifacts(product.dir)) {
|
|
966
|
+
return { remove: true, reason: "current-product-no-local-artifacts" };
|
|
967
|
+
}
|
|
968
|
+
if (!localArtifactsReferenceContainer(product.dir, container.name)) {
|
|
969
|
+
return { remove: true, reason: "current-product-unreferenced" };
|
|
970
|
+
}
|
|
971
|
+
return { remove: false, reason: "current-product-referenced" };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (options.global && containerProductDir && !fs.existsSync(containerProductDir)) {
|
|
975
|
+
return { remove: true, reason: "product-dir-missing" };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (options.global && containerProductDir && !hasRemainingLocalArtifacts(containerProductDir)) {
|
|
979
|
+
return { remove: true, reason: "product-has-no-local-artifacts" };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return { remove: false, reason: options.global ? "other-product-retained" : "different-product" };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function isManagedLocalPostgresContainer(container) {
|
|
986
|
+
const labels = container.labels || {};
|
|
987
|
+
return (
|
|
988
|
+
labels[TESTKIT_RESOURCE_KIND_LABEL] === "postgres-container" &&
|
|
989
|
+
labels[TESTKIT_SCOPE_LABEL] === "local-postgres"
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function buildLocalPostgresContainerLabels(productDir, containerName) {
|
|
994
|
+
return buildDockerResourceLabels(productDir, {
|
|
995
|
+
kind: "postgres-container",
|
|
996
|
+
name: containerName,
|
|
997
|
+
scope: "local-postgres",
|
|
998
|
+
cachePolicy: "product-cache",
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function localArtifactsReferenceContainer(productDir, containerName) {
|
|
1003
|
+
if (!containerName) return false;
|
|
1004
|
+
const root = path.join(productDir, ".testkit");
|
|
1005
|
+
let referenced = false;
|
|
1006
|
+
visitDirs(root, (dir) => {
|
|
1007
|
+
if (referenced) return;
|
|
1008
|
+
for (const fileName of ["container_name", "local_container_name"]) {
|
|
1009
|
+
if (readStateValue(path.join(dir, fileName)) === containerName) {
|
|
1010
|
+
referenced = true;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
return referenced;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function formatDatabaseResourceCleanupLine(entry, dryRun = false) {
|
|
1019
|
+
const action = dryRun ? "Would remove" : "Removed";
|
|
1020
|
+
const legacy = entry.legacy ? " legacy" : "";
|
|
1021
|
+
return `${action}${legacy} database resource ${dockerContainerSummary(entry)} reason=${entry.reason}`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
868
1024
|
function buildContainerName(productDir) {
|
|
869
1025
|
return buildContainerNameModel(productDir);
|
|
870
1026
|
}
|
|
@@ -878,9 +1034,11 @@ function buildRuntimeDatabaseName(serviceName, bindingKey, fingerprint) {
|
|
|
878
1034
|
}
|
|
879
1035
|
|
|
880
1036
|
function writeLocalInfraState(infraDir, infra) {
|
|
1037
|
+
const product = buildProductIdentity(path.resolve(infraDir, "..", "..", ".."));
|
|
881
1038
|
fs.writeFileSync(path.join(infraDir, "database_backend"), "local");
|
|
882
1039
|
fs.writeFileSync(path.join(infraDir, "container_name"), infra.containerName);
|
|
883
1040
|
fs.writeFileSync(path.join(infraDir, "container_id"), infra.containerId);
|
|
1041
|
+
fs.writeFileSync(path.join(infraDir, "ownership_product_id"), product.id);
|
|
884
1042
|
fs.writeFileSync(path.join(infraDir, "image"), infra.image);
|
|
885
1043
|
fs.writeFileSync(path.join(infraDir, "user"), infra.user);
|
|
886
1044
|
fs.writeFileSync(path.join(infraDir, "password"), infra.password);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
|
|
6
|
+
export const TESTKIT_MANAGED_LABEL = "com.elench.testkit.managed";
|
|
7
|
+
export const TESTKIT_RESOURCE_KIND_LABEL = "com.elench.testkit.resource.kind";
|
|
8
|
+
export const TESTKIT_RESOURCE_NAME_LABEL = "com.elench.testkit.resource.name";
|
|
9
|
+
export const TESTKIT_PRODUCT_DIR_LABEL = "com.elench.testkit.product.dir";
|
|
10
|
+
export const TESTKIT_PRODUCT_ID_LABEL = "com.elench.testkit.product.id";
|
|
11
|
+
export const TESTKIT_SCOPE_LABEL = "com.elench.testkit.scope";
|
|
12
|
+
export const TESTKIT_CACHE_POLICY_LABEL = "com.elench.testkit.cache.policy";
|
|
13
|
+
export const TESTKIT_CREATED_AT_LABEL = "com.elench.testkit.created-at";
|
|
14
|
+
|
|
15
|
+
const TESTKIT_POSTGRES_CONTAINER_PREFIX = "testkit_pg_";
|
|
16
|
+
|
|
17
|
+
export function buildProductIdentity(productDir) {
|
|
18
|
+
const canonicalDir = canonicalizeProductDir(productDir);
|
|
19
|
+
return {
|
|
20
|
+
dir: canonicalDir,
|
|
21
|
+
id: hashString(canonicalDir, 24),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildDockerResourceLabels(productDir, resource) {
|
|
26
|
+
const product = buildProductIdentity(productDir);
|
|
27
|
+
return {
|
|
28
|
+
[TESTKIT_MANAGED_LABEL]: "true",
|
|
29
|
+
[TESTKIT_RESOURCE_KIND_LABEL]: resource.kind,
|
|
30
|
+
[TESTKIT_RESOURCE_NAME_LABEL]: resource.name,
|
|
31
|
+
[TESTKIT_PRODUCT_DIR_LABEL]: product.dir,
|
|
32
|
+
[TESTKIT_PRODUCT_ID_LABEL]: product.id,
|
|
33
|
+
[TESTKIT_SCOPE_LABEL]: resource.scope || resource.kind,
|
|
34
|
+
[TESTKIT_CACHE_POLICY_LABEL]: resource.cachePolicy || "ephemeral",
|
|
35
|
+
[TESTKIT_CREATED_AT_LABEL]: resource.createdAt || new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function dockerLabelArgs(labels) {
|
|
40
|
+
return Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${String(value)}`]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function inspectDockerContainer(containerRef) {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execa("docker", ["inspect", containerRef]);
|
|
46
|
+
const parsed = JSON.parse(stdout);
|
|
47
|
+
return parsed[0] || null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function listManagedDockerContainers() {
|
|
54
|
+
return listDockerContainers([
|
|
55
|
+
"--filter",
|
|
56
|
+
`label=${TESTKIT_MANAGED_LABEL}=true`,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function listLegacyTestkitPostgresContainers() {
|
|
61
|
+
const containers = await listDockerContainers([]);
|
|
62
|
+
return containers.filter((container) => {
|
|
63
|
+
if (!container.name.startsWith(TESTKIT_POSTGRES_CONTAINER_PREFIX)) return false;
|
|
64
|
+
return container.labels[TESTKIT_MANAGED_LABEL] !== "true";
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function removeDockerContainer(containerRef) {
|
|
69
|
+
try {
|
|
70
|
+
await execa("docker", ["rm", "-f", containerRef]);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function dockerContainerSummary(container) {
|
|
78
|
+
const status = container.running ? "running" : "stopped";
|
|
79
|
+
return `${container.name} (${status})`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function listDockerContainers(filters) {
|
|
83
|
+
let stdout = "";
|
|
84
|
+
try {
|
|
85
|
+
const result = await execa("docker", [
|
|
86
|
+
"ps",
|
|
87
|
+
"-a",
|
|
88
|
+
"--format",
|
|
89
|
+
"{{.ID}}",
|
|
90
|
+
...filters,
|
|
91
|
+
]);
|
|
92
|
+
stdout = result.stdout;
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ids = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
98
|
+
if (ids.length === 0) return [];
|
|
99
|
+
|
|
100
|
+
const containers = [];
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
const inspect = await inspectDockerContainer(id);
|
|
103
|
+
if (inspect) containers.push(inspect);
|
|
104
|
+
}
|
|
105
|
+
return containers.map(normalizeInspectContainer).filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeInspectContainer(inspect) {
|
|
109
|
+
if (!inspect?.Id) return null;
|
|
110
|
+
const name = String(inspect.Name || "").replace(/^\//, "");
|
|
111
|
+
const labels = inspect.Config?.Labels || {};
|
|
112
|
+
return {
|
|
113
|
+
id: inspect.Id,
|
|
114
|
+
shortId: inspect.Id.slice(0, 12),
|
|
115
|
+
name,
|
|
116
|
+
image: inspect.Config?.Image || "",
|
|
117
|
+
createdAt: inspect.Created || null,
|
|
118
|
+
running: Boolean(inspect.State?.Running),
|
|
119
|
+
status: inspect.State?.Status || "unknown",
|
|
120
|
+
labels,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function canonicalizeProductDir(productDir) {
|
|
125
|
+
const resolved = path.resolve(productDir || process.cwd());
|
|
126
|
+
try {
|
|
127
|
+
return fs.realpathSync.native(resolved);
|
|
128
|
+
} catch {
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hashString(value, length = 24) {
|
|
134
|
+
return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length);
|
|
135
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import {
|
|
4
|
+
cleanupGlobalLocalDatabaseResources,
|
|
5
|
+
cleanupOwnedLocalDatabaseResources,
|
|
4
6
|
cleanupOrphanedLocalInfrastructure,
|
|
5
7
|
destroyRuntimeDatabase,
|
|
6
8
|
destroyServiceDatabaseCache,
|
|
9
|
+
formatDatabaseResourceCleanupLine,
|
|
7
10
|
isDatabaseStateDir,
|
|
8
11
|
} from "../database/index.mjs";
|
|
9
12
|
import { cleanupRuns, formatRunSummary, isPidRunning, listRunManifests } from "./lifecycle.mjs";
|
|
@@ -51,6 +54,7 @@ export async function cleanup(productDir, options = {}) {
|
|
|
51
54
|
const allConfigs = options.allConfigs || [];
|
|
52
55
|
const serviceName = options.serviceName || null;
|
|
53
56
|
const cache = normalizeCacheSelection(options.cache);
|
|
57
|
+
const cleanResources = Boolean(options.resources || options.globalResources || options.includeLegacyResources);
|
|
54
58
|
const summary = dryRun
|
|
55
59
|
? collectRunCleanupPreview(productDir)
|
|
56
60
|
: await cleanupRuns(productDir, { includeActive: false });
|
|
@@ -66,6 +70,7 @@ export async function cleanup(productDir, options = {}) {
|
|
|
66
70
|
const runtimeCleaned = [];
|
|
67
71
|
const bundleCleaned = [];
|
|
68
72
|
const assistantCleaned = [];
|
|
73
|
+
let resourceCleanup = { targets: [], removed: [], kept: [] };
|
|
69
74
|
|
|
70
75
|
if (!dryRun) {
|
|
71
76
|
for (const target of targets.runtime) {
|
|
@@ -83,6 +88,19 @@ export async function cleanup(productDir, options = {}) {
|
|
|
83
88
|
pruneKnownEmptyDirs(productDir);
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
if (cleanResources) {
|
|
92
|
+
resourceCleanup = options.globalResources
|
|
93
|
+
? await cleanupGlobalLocalDatabaseResources({
|
|
94
|
+
productDir,
|
|
95
|
+
dryRun,
|
|
96
|
+
includeLegacy: Boolean(options.includeLegacyResources),
|
|
97
|
+
})
|
|
98
|
+
: await cleanupOwnedLocalDatabaseResources(productDir, {
|
|
99
|
+
dryRun,
|
|
100
|
+
includeLegacy: Boolean(options.includeLegacyResources),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
const lines = [];
|
|
87
105
|
for (const manifest of summary.cleaned) {
|
|
88
106
|
lines.push(`${dryRun ? "Would clean" : "Cleaned"} stale run ${formatRunSummary(manifest)}`);
|
|
@@ -108,6 +126,9 @@ export async function cleanup(productDir, options = {}) {
|
|
|
108
126
|
label: "assistant session",
|
|
109
127
|
dryRun,
|
|
110
128
|
});
|
|
129
|
+
for (const target of resourceCleanup.targets) {
|
|
130
|
+
lines.push(formatDatabaseResourceCleanupLine(target, dryRun));
|
|
131
|
+
}
|
|
111
132
|
|
|
112
133
|
if (lines.length === 0) {
|
|
113
134
|
return {
|
|
@@ -118,6 +139,7 @@ export async function cleanup(productDir, options = {}) {
|
|
|
118
139
|
bundleCleaned,
|
|
119
140
|
assistantCleaned,
|
|
120
141
|
localCleaned,
|
|
142
|
+
resourceCleanup,
|
|
121
143
|
lines: ["No stale runs to clean."],
|
|
122
144
|
};
|
|
123
145
|
}
|
|
@@ -130,6 +152,7 @@ export async function cleanup(productDir, options = {}) {
|
|
|
130
152
|
bundleCleaned,
|
|
131
153
|
assistantCleaned,
|
|
132
154
|
localCleaned,
|
|
155
|
+
resourceCleanup,
|
|
133
156
|
lines,
|
|
134
157
|
};
|
|
135
158
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.145",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.145"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.145",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -98,10 +98,10 @@
|
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
100
|
"@babel/code-frame": "^7.29.0",
|
|
101
|
-
"@elench/next-analysis": "0.1.
|
|
102
|
-
"@elench/testkit-bridge": "0.1.
|
|
103
|
-
"@elench/testkit-protocol": "0.1.
|
|
104
|
-
"@elench/ts-analysis": "0.1.
|
|
101
|
+
"@elench/next-analysis": "0.1.145",
|
|
102
|
+
"@elench/testkit-bridge": "0.1.145",
|
|
103
|
+
"@elench/testkit-protocol": "0.1.145",
|
|
104
|
+
"@elench/ts-analysis": "0.1.145",
|
|
105
105
|
"@oclif/core": "^4.10.6",
|
|
106
106
|
"@playwright/test": "^1.52.0",
|
|
107
107
|
"esbuild": "^0.25.11",
|