fix(runtime): align startup brief and test status paths
--- Build: pass | Tests: pass — Tests 1951 passed (1951)
This commit is contained in:
parent
fe14fadc1c
commit
1389e17ec4
12 changed files with 243 additions and 93 deletions
|
|
@ -7,15 +7,16 @@
|
|||
# scripts/write-test-build-status.sh build # run build only
|
||||
# scripts/write-test-build-status.sh tests # run tests only
|
||||
#
|
||||
# Status dir resolves to $CLAWDIE_VAR_DIR or $HOME/.clawdie/status,
|
||||
# matching getDefaultStatusDir() in src/reports/test-report.ts.
|
||||
# Status dir resolves to $AGENT_STATUS_DIR, then the legacy $CLAWDIE_VAR_DIR,
|
||||
# and otherwise defaults to the repo-local tmp/status directory, matching
|
||||
# getDefaultStatusDir() in src/reports/test-report.ts.
|
||||
|
||||
set -u
|
||||
|
||||
REPO_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
STATUS_DIR=${CLAWDIE_VAR_DIR:-"${HOME:-/var/db/clawdie}/.clawdie/status"}
|
||||
STATUS_DIR=${AGENT_STATUS_DIR:-${CLAWDIE_VAR_DIR:-"$REPO_ROOT/tmp/status"}}
|
||||
mkdir -p "$STATUS_DIR"
|
||||
TMP_DIR="$REPO_ROOT/tmp/test-build-status"
|
||||
mkdir -p "$TMP_DIR"
|
||||
|
|
|
|||
|
|
@ -53,12 +53,12 @@ describe('config identity', () => {
|
|||
expect(config.AGENT_INTERNAL_DOMAIN).toBe('mevy-agent.home.arpa');
|
||||
});
|
||||
|
||||
it('defaults tenant identity to clawdie when TENANT_ID is unset', async () => {
|
||||
it('falls back to the configured runtime tenant when TENANT_ID is unset', async () => {
|
||||
process.env.ASSISTANT_NAME = 'Bob';
|
||||
|
||||
const config = await import('./config.js');
|
||||
|
||||
expect(config.TENANT_ID).toBe('clawdie');
|
||||
expect(config.TENANT_ID).toBe('mevy');
|
||||
expect(config.TENANT_DISPLAY_NAME).toBe('Bob');
|
||||
expect(config.ASSISTANT_NAME).toBe('Bob');
|
||||
expect(config.AGENT_DOMAIN).toBe('home.arpa');
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ describe('getDefaultAgents', () => {
|
|||
});
|
||||
|
||||
it('DEFAULT_AGENTS uses TENANT_ID as orchestrator id', () => {
|
||||
expect(DEFAULT_AGENTS[0].id).toBe('clawdie');
|
||||
expect(DEFAULT_AGENTS[0].id).toBe('mevy');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -139,40 +139,72 @@ describe('dashboard-view', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('summarizes tenant-site rows for dashboard hero cards', () => {
|
||||
const summary = buildDashboardSummary(
|
||||
buildDashboardTenantSiteRows(makeRegistry(), '/nonexistent-webroot'),
|
||||
it('summarizes tenant-site rows for dashboard hero cards', async () => {
|
||||
const statusRoot = path.join(
|
||||
process.cwd(),
|
||||
'tmp',
|
||||
'dashboard-view-empty-status-root',
|
||||
);
|
||||
fs.rmSync(statusRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(statusRoot, { recursive: true });
|
||||
process.env.TENANT_SITE_PUBLISH_STATUS_ROOT = statusRoot;
|
||||
try {
|
||||
vi.resetModules();
|
||||
const { buildDashboardSummary: buildSummary, buildDashboardTenantSiteRows: buildRows } =
|
||||
await import('./dashboard-view.js');
|
||||
const summary = buildSummary(
|
||||
buildRows(makeRegistry(), '/nonexistent-webroot'),
|
||||
);
|
||||
|
||||
expect(summary).toEqual({
|
||||
tenantCount: 1,
|
||||
siteCount: 1,
|
||||
availableSiteCount: 0,
|
||||
plannedSiteCount: 1,
|
||||
strapiSnapshotSiteCount: 1,
|
||||
generatedSiteCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('builds tenant rollups for operator cards', () => {
|
||||
const rows = buildDashboardTenantRows(
|
||||
buildDashboardTenantSiteRows(makeRegistry(), '/nonexistent-webroot'),
|
||||
);
|
||||
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
tenantId: 'mevy',
|
||||
tenantDisplayName: 'Mevy',
|
||||
homeHost: 'mevy.home.arpa',
|
||||
expect(summary).toEqual({
|
||||
tenantCount: 1,
|
||||
siteCount: 1,
|
||||
availableSiteCount: 0,
|
||||
plannedSiteCount: 1,
|
||||
strapiSnapshotSiteCount: 1,
|
||||
generatedSiteCount: 0,
|
||||
internalSiteCount: 1,
|
||||
publicSiteCount: 0,
|
||||
disabledSiteCount: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
} finally {
|
||||
delete process.env.TENANT_SITE_PUBLISH_STATUS_ROOT;
|
||||
fs.rmSync(statusRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('builds tenant rollups for operator cards', async () => {
|
||||
const statusRoot = path.join(
|
||||
process.cwd(),
|
||||
'tmp',
|
||||
'dashboard-view-empty-status-root-2',
|
||||
);
|
||||
fs.rmSync(statusRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(statusRoot, { recursive: true });
|
||||
process.env.TENANT_SITE_PUBLISH_STATUS_ROOT = statusRoot;
|
||||
try {
|
||||
vi.resetModules();
|
||||
const { buildDashboardTenantRows: buildRowsByTenant, buildDashboardTenantSiteRows: buildRows } =
|
||||
await import('./dashboard-view.js');
|
||||
const rows = buildRowsByTenant(
|
||||
buildRows(makeRegistry(), '/nonexistent-webroot'),
|
||||
);
|
||||
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
tenantId: 'mevy',
|
||||
tenantDisplayName: 'Mevy',
|
||||
homeHost: 'mevy.home.arpa',
|
||||
siteCount: 1,
|
||||
availableSiteCount: 0,
|
||||
plannedSiteCount: 1,
|
||||
strapiSnapshotSiteCount: 1,
|
||||
generatedSiteCount: 0,
|
||||
internalSiteCount: 1,
|
||||
publicSiteCount: 0,
|
||||
disabledSiteCount: 0,
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
delete process.env.TENANT_SITE_PUBLISH_STATUS_ROOT;
|
||||
fs.rmSync(statusRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ vi.mock('./authorized-hostd.js', () => ({
|
|||
describe('platform-audit-report', () => {
|
||||
it('collects observed service, jail, and dataset ownership', async () => {
|
||||
// Service probes run in alphabetical order over the deduped union of
|
||||
// shared.services + tenant services: clawdie, clawdie_hostd, cms,
|
||||
// code-service, mevy, postgresql, web-service. Then jails, then datasets.
|
||||
// shared.services + tenant services: cms, code-service, mevy,
|
||||
// mevy_hostd, postgresql, web-service. Then jails, then datasets.
|
||||
callAuthorizedHostdMock
|
||||
.mockResolvedValueOnce({ ok: false, error: 'clawdie is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'clawdie_hostd is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'cms is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'code-service is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'mevy is not running' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'mevy_hostd is not running' })
|
||||
.mockResolvedValueOnce({ ok: true, output: 'postgresql is running as pid 1' })
|
||||
.mockResolvedValueOnce({ ok: false, error: 'web-service is not running' })
|
||||
.mockResolvedValueOnce({
|
||||
|
|
@ -34,7 +33,7 @@ describe('platform-audit-report', () => {
|
|||
caller: 'operator',
|
||||
});
|
||||
|
||||
expect(report.observedServices).toHaveLength(7);
|
||||
expect(report.observedServices).toHaveLength(6);
|
||||
expect(report.observedJails).toEqual({
|
||||
shared: ['git'],
|
||||
tenants: { mevy: ['mevy_ctrl_worker'] },
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ describe('platform-audit', () => {
|
|||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('mevy', registry)).toBe('tenant:mevy');
|
||||
expect(classifyServiceOwner('clawdie_hostd', registry)).toBe(
|
||||
expect(classifyServiceOwner('mevy_hostd', registry)).toBe(
|
||||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('unknown-service', registry)).toBe('unknown');
|
||||
});
|
||||
|
||||
it('shared check wins when a service is both shared and declared by a tenant', () => {
|
||||
expect(classifyServiceOwner('clawdie', registry)).toBe('shared-platform');
|
||||
it('classifies shared services before unknown fallthrough', () => {
|
||||
expect(classifyServiceOwner('cms', registry)).toBe('shared-platform');
|
||||
});
|
||||
|
||||
it('classifies shared and tenant-owned jails', () => {
|
||||
|
|
@ -52,7 +52,7 @@ describe('platform-audit', () => {
|
|||
|
||||
it('summarizes ownership for a tenant', () => {
|
||||
const summary = summarizeOwnership(
|
||||
['postgresql', 'mevy', 'clawdie', 'mystery'],
|
||||
['postgresql', 'mevy', 'mevy_hostd', 'mystery'],
|
||||
classifyServiceOwner,
|
||||
registry,
|
||||
'mevy',
|
||||
|
|
@ -73,10 +73,10 @@ describe('platform-audit', () => {
|
|||
});
|
||||
|
||||
it('normalizes service-name comparisons (case + separator differences)', () => {
|
||||
expect(classifyServiceOwner('Clawdie_HostD', registry)).toBe(
|
||||
expect(classifyServiceOwner('Mevy_HostD', registry)).toBe(
|
||||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('clawdie-hostd', registry)).toBe(
|
||||
expect(classifyServiceOwner('mevy-hostd', registry)).toBe(
|
||||
'shared-platform',
|
||||
);
|
||||
expect(classifyServiceOwner('MEVY', registry)).toBe('tenant:mevy');
|
||||
|
|
@ -105,7 +105,7 @@ describe('platform-audit', () => {
|
|||
|
||||
it('buckets observed resources by owner', () => {
|
||||
const buckets = bucketOwnedResources(
|
||||
['git', 'mevy_ctrl_worker', 'clawdie_hostd', 'mystery'],
|
||||
['git', 'mevy_ctrl_worker', 'mevy_hostd', 'mystery'],
|
||||
(item, currentRegistry) => {
|
||||
if (item.endsWith('_hostd'))
|
||||
return classifyServiceOwner(item, currentRegistry);
|
||||
|
|
@ -115,7 +115,7 @@ describe('platform-audit', () => {
|
|||
);
|
||||
|
||||
expect(buckets).toEqual({
|
||||
shared: ['clawdie_hostd', 'git'],
|
||||
shared: ['git', 'mevy_hostd'],
|
||||
tenants: {
|
||||
mevy: ['mevy_ctrl_worker'],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import {
|
||||
buildTestReport,
|
||||
getDefaultBuildStatusPath,
|
||||
getDefaultStatusDir,
|
||||
getDefaultTestStatusPath,
|
||||
loadTestBuildSource,
|
||||
parseTestBuildStatus,
|
||||
renderTestReport,
|
||||
|
|
@ -63,6 +66,30 @@ describe('parseTestBuildStatus', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('default status paths', () => {
|
||||
it('defaults to repo-local tmp/status when no override is set', () => {
|
||||
const previousAgent = process.env.AGENT_STATUS_DIR;
|
||||
const previousLegacy = process.env.CLAWDIE_VAR_DIR;
|
||||
delete process.env.AGENT_STATUS_DIR;
|
||||
delete process.env.CLAWDIE_VAR_DIR;
|
||||
|
||||
try {
|
||||
expect(getDefaultStatusDir()).toBe(path.resolve(process.cwd(), 'tmp', 'status'));
|
||||
expect(getDefaultBuildStatusPath()).toBe(
|
||||
path.resolve(process.cwd(), 'tmp', 'status', 'build-status.json'),
|
||||
);
|
||||
expect(getDefaultTestStatusPath()).toBe(
|
||||
path.resolve(process.cwd(), 'tmp', 'status', 'test-status.json'),
|
||||
);
|
||||
} finally {
|
||||
if (previousAgent === undefined) delete process.env.AGENT_STATUS_DIR;
|
||||
else process.env.AGENT_STATUS_DIR = previousAgent;
|
||||
if (previousLegacy === undefined) delete process.env.CLAWDIE_VAR_DIR;
|
||||
else process.env.CLAWDIE_VAR_DIR = previousLegacy;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadTestBuildSource', () => {
|
||||
let tmpDir: string | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { PROJECT_ROOT } from '../config.js';
|
||||
import { formatDisplayDate } from '../display-date.js';
|
||||
|
||||
export type TestBuildOutcome = 'ok' | 'fail' | 'unknown';
|
||||
|
|
@ -161,10 +162,10 @@ export async function loadTestBuildSource(inputs: {
|
|||
}
|
||||
|
||||
export function getDefaultStatusDir(): string {
|
||||
const explicit = process.env.CLAWDIE_VAR_DIR;
|
||||
const explicit =
|
||||
process.env.AGENT_STATUS_DIR || process.env.CLAWDIE_VAR_DIR;
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const home = process.env.HOME || '/var/db/clawdie';
|
||||
return path.join(home, '.clawdie', 'status');
|
||||
return path.join(PROJECT_ROOT, 'tmp', 'status');
|
||||
}
|
||||
|
||||
export function getDefaultBuildStatusPath(): string {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildAiTokenBriefLines,
|
||||
buildOwnershipSectionLines,
|
||||
readCurrentCommitHash,
|
||||
formatTimestamp,
|
||||
parseUptime,
|
||||
parseJails,
|
||||
|
|
@ -87,7 +91,7 @@ describe('buildAiTokenBriefLines', () => {
|
|||
describe('buildOwnershipSectionLines', () => {
|
||||
it('includes controlplane addresses and tenant sites', () => {
|
||||
const lines = buildOwnershipSectionLines({
|
||||
platformId: 'clawdie',
|
||||
platformId: 'mevy',
|
||||
tenantId: 'mevy',
|
||||
tenantDisplayName: 'Mevy',
|
||||
serviceNames: ['clawdie', 'cms'],
|
||||
|
|
@ -97,6 +101,7 @@ describe('buildOwnershipSectionLines', () => {
|
|||
|
||||
expect(lines).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Platform: Clawdie (clawdie)',
|
||||
'Controlplane (internal): ai.home.arpa',
|
||||
'Tenant home: mevy.home.arpa',
|
||||
'Tenant sites: blog → blog.mevy.home.arpa',
|
||||
|
|
@ -156,22 +161,22 @@ describe('parseUptime', () => {
|
|||
});
|
||||
|
||||
describe('parseJails', () => {
|
||||
it('parses bastille list all output', () => {
|
||||
it('parses bastille list all output and uses canonical jail names from path', () => {
|
||||
const raw = [
|
||||
'JID Hostname IP State Release Port',
|
||||
'1 db.home.arpa 10.0.0.3 Up 15.0-RELEASE -',
|
||||
'2 cms.home.arpa 10.0.0.4 Up 15.0-RELEASE -',
|
||||
'3 git.home.arpa 10.0.0.6 Down 15.0-RELEASE -',
|
||||
'JID Boot Prio State IP Address Published Ports Hostname Release Path',
|
||||
'1 on 99 Up 10.0.1.4 - cms.mevy.home.arpa 15.0-RELEASE-p4 /usr/local/bastille/jails/cms/root',
|
||||
'2 on 99 Up 10.0.1.213 - ctrl-worker.mevy.home.arpa 15.0-RELEASE-p4 /usr/local/bastille/jails/mevy_ctrl_worker/root',
|
||||
'3 on 99 Down 10.0.1.212 - git-worker.mevy.home.arpa 15.0-RELEASE-p4 /usr/local/bastille/jails/mevy_git_worker/root',
|
||||
].join('\n');
|
||||
|
||||
const result = parseJails(raw);
|
||||
expect(result.up).toBe(2);
|
||||
expect(result.total).toBe(3);
|
||||
expect(result.names).toEqual(['db', 'cms', 'git']);
|
||||
expect(result.names).toEqual(['cms', 'mevy_ctrl_worker', 'mevy_git_worker']);
|
||||
expect(result.details).toContain('🌐');
|
||||
expect(result.details).toContain('🧬');
|
||||
expect(result.details).toContain('db');
|
||||
expect(result.details).toContain('git');
|
||||
expect(result.details).toContain('🛠');
|
||||
expect(result.details).toContain('mevy_ctrl_worker');
|
||||
expect(result.details).toContain('mevy_git_worker');
|
||||
});
|
||||
|
||||
it('handles empty jail list', () => {
|
||||
|
|
@ -183,6 +188,36 @@ describe('parseJails', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('readCurrentCommitHash', () => {
|
||||
it('reads the current branch ref instead of assuming main', () => {
|
||||
const root = path.resolve(process.cwd(), 'tmp', 'startup-report-git-ref');
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(root, '.git', 'refs', 'heads'), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(path.join(root, '.git', 'HEAD'), 'ref: refs/heads/multitenant\n');
|
||||
fs.writeFileSync(
|
||||
path.join(root, '.git', 'refs', 'heads', 'multitenant'),
|
||||
'1234567890abcdef\n',
|
||||
);
|
||||
|
||||
expect(readCurrentCommitHash(root)).toBe('1234567');
|
||||
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reads detached HEAD hashes directly', () => {
|
||||
const root = path.resolve(process.cwd(), 'tmp', 'startup-report-git-detached');
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(root, '.git'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, '.git', 'HEAD'), 'abcdef0123456789\n');
|
||||
|
||||
expect(readCurrentCommitHash(root)).toBe('abcdef0');
|
||||
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseZpool', () => {
|
||||
it('parses zpool list output', () => {
|
||||
const raw = 'zroot\tONLINE\t50G\t20G\t30G';
|
||||
|
|
@ -333,7 +368,7 @@ describe('formatMemorySectionLines', () => {
|
|||
describe('buildOwnershipSectionLines', () => {
|
||||
it('summarizes tenant, shared, and unknown resources', () => {
|
||||
const lines = buildOwnershipSectionLines({
|
||||
platformId: 'clawdie',
|
||||
platformId: 'mevy',
|
||||
tenantId: 'mevy',
|
||||
tenantDisplayName: 'Mevy',
|
||||
serviceNames: ['postgresql', 'mevy', 'mysteryd'],
|
||||
|
|
|
|||
|
|
@ -128,6 +128,56 @@ export function parseUptime(bootRaw: string): string {
|
|||
return d > 0 ? `${d}d ${h}h` : `${h}h`;
|
||||
}
|
||||
|
||||
function resolveGitDir(projectRoot: string): string | null {
|
||||
try {
|
||||
const dotGitPath = path.join(projectRoot, '.git');
|
||||
const stat = fs.statSync(dotGitPath);
|
||||
if (stat.isDirectory()) return dotGitPath;
|
||||
if (!stat.isFile()) return null;
|
||||
const raw = fs.readFileSync(dotGitPath, 'utf8').trim();
|
||||
const match = raw.match(/^gitdir:\s*(.+)$/iu);
|
||||
if (!match) return null;
|
||||
return path.resolve(projectRoot, match[1].trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readPackedRef(gitDir: string, refName: string): string | null {
|
||||
try {
|
||||
const packedRefs = fs.readFileSync(path.join(gitDir, 'packed-refs'), 'utf8');
|
||||
for (const line of packedRefs.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('^')) continue;
|
||||
const [hash, ref] = trimmed.split(/\s+/u);
|
||||
if (ref === refName && hash) return hash.trim();
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function readCurrentCommitHash(projectRoot: string = PROJECT_ROOT): string {
|
||||
try {
|
||||
const gitDir = resolveGitDir(projectRoot);
|
||||
if (!gitDir) return '?';
|
||||
const headRaw = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
||||
let fullHash = headRaw;
|
||||
if (headRaw.startsWith('ref:')) {
|
||||
const refName = headRaw.slice(4).trim();
|
||||
const refPath = path.join(gitDir, refName);
|
||||
try {
|
||||
fullHash = fs.readFileSync(refPath, 'utf8').trim();
|
||||
} catch {
|
||||
fullHash = readPackedRef(gitDir, refName) || '';
|
||||
}
|
||||
}
|
||||
return fullHash ? fullHash.slice(0, 7) : '?';
|
||||
} catch {
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
function jailIcon(name: string): string {
|
||||
const n = name.toLowerCase();
|
||||
|
|
@ -149,20 +199,26 @@ export function parseJails(jailRaw: string): {
|
|||
.split('\n')
|
||||
.filter((l) => l.trim() && !l.trim().startsWith('JID'));
|
||||
const names: string[] = [];
|
||||
const up = dataLines.filter((l) => /\bUp\b/i.test(l)).length;
|
||||
const details = dataLines
|
||||
.map((l) => {
|
||||
const cols = l.trim().split(/\s{2,}/);
|
||||
const state = cols[3] || '?';
|
||||
const hostname = cols[6] || cols[1] || '?';
|
||||
const name = hostname.split('.')[0];
|
||||
names.push(name);
|
||||
const isUp = /\bUp\b/i.test(state);
|
||||
const icon = jailIcon(name);
|
||||
return ` ${icon} ${name}${isUp ? '' : ' (down)'}`;
|
||||
})
|
||||
.join('\n');
|
||||
return { up, total: dataLines.length, details, names };
|
||||
const rendered: string[] = [];
|
||||
let up = 0;
|
||||
|
||||
for (const line of dataLines) {
|
||||
const cols = line.trim().split(/\s{2,}/);
|
||||
const state = cols[3] || '?';
|
||||
const hostname = cols[6] || cols[1] || '?';
|
||||
const jailPath = cols[8] || '';
|
||||
const pathName =
|
||||
jailPath.match(/\/jails\/([^/]+)\/root$/u)?.[1] || '';
|
||||
const fallbackName = hostname.split('.')[0];
|
||||
const name = pathName || fallbackName;
|
||||
const isUp = /\bUp\b/i.test(state);
|
||||
if (isUp) up += 1;
|
||||
names.push(name);
|
||||
const icon = jailIcon(name);
|
||||
rendered.push(` ${icon} ${name}${isUp ? '' : ' (down)'}`);
|
||||
}
|
||||
|
||||
return { up, total: dataLines.length, details: rendered.join('\n'), names };
|
||||
}
|
||||
|
||||
export function deriveRepoWebBase(remote: string): string {
|
||||
|
|
@ -324,6 +380,7 @@ export function buildOwnershipSectionLines(opts: {
|
|||
datasets: string[];
|
||||
}): string[] {
|
||||
const registry = loadTenantRegistry();
|
||||
const canonicalPlatformId = registry.platform.id;
|
||||
const tenant = getTenantRecord(opts.tenantId, registry);
|
||||
const surfaces = buildSurfaceInventory(registry);
|
||||
const controlplaneInternal = surfaces.find(
|
||||
|
|
@ -343,7 +400,7 @@ export function buildOwnershipSectionLines(opts: {
|
|||
|
||||
const lines = [
|
||||
...sectionHeader('🧭 Platform'),
|
||||
`Platform: ${registry.platform.displayName} (${opts.platformId})`,
|
||||
`Platform: ${registry.platform.displayName} (${canonicalPlatformId})`,
|
||||
`Controlplane (internal): ${controlplaneInternal || registry.platform.internalDomain}`,
|
||||
...(controlplanePublic
|
||||
? [`Controlplane (public): ${controlplanePublic}`]
|
||||
|
|
@ -361,7 +418,7 @@ export function buildOwnershipSectionLines(opts: {
|
|||
|
||||
const tenantWarning = tenantContextWarning(
|
||||
opts.tenantId,
|
||||
opts.platformId,
|
||||
canonicalPlatformId,
|
||||
!!tenant,
|
||||
);
|
||||
if (tenantWarning) {
|
||||
|
|
@ -544,16 +601,7 @@ export function buildStartupReport(): string {
|
|||
const now = new Date();
|
||||
const timestamp = formatTimestamp(now, tz, locale);
|
||||
|
||||
const commitHash = (() => {
|
||||
try {
|
||||
return fs
|
||||
.readFileSync(path.join(PROJECT_ROOT, '.git/refs/heads/main'), 'utf-8')
|
||||
.trim()
|
||||
.slice(0, 7);
|
||||
} catch {
|
||||
return '?';
|
||||
}
|
||||
})();
|
||||
const commitHash = readCurrentCommitHash(PROJECT_ROOT);
|
||||
const repoBase = deriveRepoWebBase(REMOTE_GIT_URL);
|
||||
const commitUrl = repoBase ? `${repoBase}/commit/${commitHash}` : '';
|
||||
|
||||
|
|
@ -619,6 +667,13 @@ export function buildStartupReport(): string {
|
|||
shellExec('sudo pkg version -vRUL= 2>/dev/null'),
|
||||
);
|
||||
const registry = loadTenantRegistry();
|
||||
const tenant = getTenantRecord(TENANT_ID, registry);
|
||||
const declaredServiceNames = Array.from(
|
||||
new Set([
|
||||
...(tenant?.service ? [tenant.service] : [TENANT_ID]),
|
||||
...registry.shared.services,
|
||||
]),
|
||||
);
|
||||
|
||||
const distStaleWarning = getDistStaleWarningLine();
|
||||
|
||||
|
|
@ -655,7 +710,7 @@ export function buildStartupReport(): string {
|
|||
platformId: PLATFORM_ID,
|
||||
tenantId: TENANT_ID,
|
||||
tenantDisplayName: TENANT_DISPLAY_NAME,
|
||||
serviceNames: registry.shared.services,
|
||||
serviceNames: declaredServiceNames,
|
||||
jailNames: jails.names,
|
||||
datasets: [mevyDataset, pgDataset],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1916,7 +1916,7 @@ export async function handleStatusCommand(
|
|||
`Uptime: ${h}h ${m}m`,
|
||||
`Load: <code>${load1.toFixed(2)}</code> / <code>${load5.toFixed(2)}</code> / <code>${load15.toFixed(2)}</code>`,
|
||||
`Memory: ${freeMB}MB free / ${totalMB}MB total`,
|
||||
`Identity: <code>${PLATFORM_ID}</code> platform · <code>${TENANT_ID}</code> tenant (${TENANT_DISPLAY_NAME})`,
|
||||
`Identity: <code>${loadTenantRegistry().platform.id}</code> platform · <code>${TENANT_ID}</code> tenant (${TENANT_DISPLAY_NAME})`,
|
||||
];
|
||||
|
||||
const defaultProvider = PI_TUI_PROVIDER;
|
||||
|
|
|
|||
|
|
@ -442,16 +442,16 @@ describe('tenant-registry', () => {
|
|||
expect(() =>
|
||||
addTenantRecord(
|
||||
'Bob Agent',
|
||||
{ service: 'clawdie_hostd' },
|
||||
{ service: 'mevy_hostd' },
|
||||
registryPath,
|
||||
),
|
||||
).toThrow('Tenant service conflicts with shared service: clawdie_hostd');
|
||||
).toThrow('Tenant service conflicts with shared service: mevy_hostd');
|
||||
});
|
||||
|
||||
it('rejects tenant ids reserved by shared resource aliases', () => {
|
||||
const registryPath = makeTempRegistry();
|
||||
expect(() => addTenantRecord('clawdie-hostd', {}, registryPath)).toThrow(
|
||||
'Tenant id is reserved by platform resources: clawdie-hostd',
|
||||
expect(() => addTenantRecord('mevy-hostd', {}, registryPath)).toThrow(
|
||||
'Tenant id is reserved by platform resources: mevy-hostd',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue