Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
541becb
finops: cloud billing via agent package (cloud-query), cost_daily fac…
geisbruch Sep 10, 2026
0419511
finops(cloud-query): release script — ECR Public login, multi-arch bu…
geisbruch Sep 10, 2026
7393d80
finops(cloud-query): release 0.0.1 → public.ecr.aws/nullplatform/agen…
geisbruch Sep 10, 2026
4891ccd
finops(tool-cloud-query): run the released image by immutable referen…
geisbruch Sep 10, 2026
ab43ec8
finops: per-customer role — AssumeRole ARN + ExternalId through wf1 a…
geisbruch Sep 10, 2026
a4619bf
finops: multi-account dispatcher (wf0) — per-target agent tags, packa…
geisbruch Sep 10, 2026
b652a0b
finops(wf0): spread each target into the collector inputs (forEach sp…
geisbruch Sep 10, 2026
114c697
finops(docs): IAM policies and trust policies for the cloud-query wor…
geisbruch Sep 10, 2026
b31fcbf
finops(docs/iam): scope Pod Identity trust to the cluster, AssumeRole…
geisbruch Sep 10, 2026
32c49fb
finops: setup script for the worker pod identity (Pod Identity, singl…
geisbruch Sep 11, 2026
50e691e
finops: worker identity rule matches the exact image + package slug
geisbruch Sep 11, 2026
f0e970a
finops: agent_nrn + org_nrn per target (agents registered under an ac…
geisbruch Sep 11, 2026
7758bf1
finops: publish to any engine (platform or local) with per-org vars; …
geisbruch Sep 11, 2026
36b78e7
finops: one row per subject, not per EC2 instance; worker 0.0.2 retur…
geisbruch Sep 11, 2026
6c2e5f6
finops: README — deploying to an organization (nullplatform done) + f…
geisbruch Sep 11, 2026
3426cde
finops: cloud-query 0.0.3 — RDS Performance Insights (`pi`) and Cloud…
geisbruch Sep 11, 2026
0d3d739
finops(docs/iam): the full read set for the allocator, one apply per …
geisbruch Sep 11, 2026
8d9a54c
finops(docs): mapping rules design — rules as data, inference with ev…
geisbruch Sep 11, 2026
df7ff20
finops: mapping rules as data — allocator (wf2), suggestions from evi…
geisbruch Sep 11, 2026
9b9f683
finops: daily loop wf0 → wf1 → wf2 (allocate) → suggestions; specs nu…
geisbruch Sep 11, 2026
14cab3c
finops: docs (configuration with examples, deploy steps, playbook §6)…
geisbruch Sep 11, 2026
9787947
finops: cloud-query 0.0.3 published (pi + logs); cost_daily enums for…
geisbruch Sep 11, 2026
eff1ee5
cloud-query 0.0.4: ISO timestamps under *Time keys become Date (Perfo…
geisbruch Sep 11, 2026
4dd890c
finops: cloud-query 0.0.4 published (image digest 24fcd9b3…)
geisbruch Sep 11, 2026
db9c7ee
finops: by_metric — wf1 attaches Performance Insights DB-load shares …
geisbruch Sep 11, 2026
0a1343b
finops: unowned by_metric keys become suggestion candidates; suggesti…
geisbruch Sep 11, 2026
e9956e0
finops: terminated Karpenter nodes attributed to their cluster by ins…
geisbruch Sep 11, 2026
dbcf9b5
finops(rules nullplatform): controlplane/auth/ai-agent-data/sonarqube…
geisbruch Sep 11, 2026
cfdc273
finops(rules nullplatform): fix Aurora cluster endpoints (.cluster-<id>)
geisbruch Sep 11, 2026
f9156b7
finops: Kubernetes consumption per scope (wf3) splits the cluster amo…
geisbruch Sep 11, 2026
1f20c4d
finops: application_cost_daily split by null objects — scopes (any ty…
geisbruch Sep 11, 2026
40a6e37
finops: application_cost_daily as an invoice — flat charge_items with…
geisbruch Sep 11, 2026
bcbcfc3
finops: invoice entity without consumption (k8s usage stays in raw-k8…
geisbruch Sep 11, 2026
384f244
finops: null dimensions (environment) on raw scope/service facts, on …
geisbruch Sep 11, 2026
c0d3f0c
finops(wf3): lake column is application_slug
geisbruch Sep 11, 2026
0bc7a7b
finops(wf2): keep step outputs under 1 MB (facts once, invoices in th…
geisbruch Sep 11, 2026
e2a4d0b
finops(wf2): unique ids for by_metric rows (per metric key) + stale s…
geisbruch Sep 11, 2026
9cac341
finops(spec): charge_type on cost_daily (the catalog dropped it from …
geisbruch Sep 11, 2026
1f47663
finops(wf2): write facts and invoices in batches (child takes facts[]…
geisbruch Sep 11, 2026
34b92ca
finops(wf2): output projections on the read steps (scopes, services, …
geisbruch Sep 11, 2026
402d185
finops(specs): every principal may write invoice/suggestion/rule enti…
geisbruch Sep 11, 2026
819a24f
finops(wf1): batched fact writes + stale sweep of the day's raw rows …
geisbruch Sep 11, 2026
2b4d19e
finops(docs): batches, stale sweeps and spec grants
geisbruch Sep 11, 2026
07b55d3
finops(tool): rely on np-package-call image → package.image (agents-a…
geisbruch Sep 11, 2026
b4fbd92
finops(wf1): Lambda (tagged services) attributed per scope: CE by sco…
geisbruch Sep 11, 2026
0648c97
finops: onboarding runbook (discovery → identity → specs → publish → …
geisbruch Sep 11, 2026
d2cde68
finops(setup): report-finops.py generates the dashboard definition; R…
geisbruch Sep 11, 2026
50d8c25
finops: itti onboarding — vars.itti.json, spec placeholder fix, runbo…
geisbruch Sep 11, 2026
513fba5
finops: spread allocation method + CloudWatch Logs shares by log group
geisbruch Sep 11, 2026
f1188ed
finops: CloudWatch attribution by application — log groups, EMF metri…
geisbruch Sep 11, 2026
9e87032
finops(wf1): Performance Insights ServiceType DOCDB for DocumentDB cl…
geisbruch Sep 11, 2026
406e97b
finops: itti day 1 — cluster from instance-type rows, DocumentDB spli…
geisbruch Sep 11, 2026
03b3384
finops(publish): restore decider ports without a registry; http-reque…
geisbruch Sep 11, 2026
481938b
finops(wf3): scope the scope listings to variables.org_nrn subtree; p…
geisbruch Sep 11, 2026
20b534f
finops(wf3): scope_usage_daily — per-scope consumption entity, priced…
geisbruch Sep 11, 2026
f7056ef
finops(wf3): no SQL comments in the lake query (the Lake splits on ';…
geisbruch Sep 11, 2026
192b50d
finops(wf3): read the NerdGraph status from http-request's statusCode
geisbruch Sep 11, 2026
d8d9bc8
finops(report): cost explorer — grouping selector, scope filter, pivo…
geisbruch Sep 11, 2026
6bef736
finops: account_dimensions — stamp the account's null dimensions on f…
geisbruch Sep 11, 2026
3a6b8d8
finops(docs): vars for New Relic mode and account dimensions
geisbruch Sep 11, 2026
a94c897
finops(wf2): return batches only on dry runs — a >1 MB child result w…
geisbruch Sep 11, 2026
b54ee56
finops(tests): invoices from the upsert children in the real-run allo…
geisbruch Sep 11, 2026
5d732bb
finops(report): drop the Kubernetes overhead KPI
geisbruch Sep 11, 2026
283e3be
finops(wf2): cloud databases/caches without a null service are charge…
geisbruch Sep 11, 2026
a0dff08
finops(wf2): service_kind on invoice charge items
geisbruch Sep 11, 2026
44b1f0c
finops(spec): cost_daily.bucket was never declared — the catalog drop…
geisbruch Sep 11, 2026
641407f
finops(wf2): batches_out from the allocate/invoices steps (dry runs o…
geisbruch Sep 11, 2026
a7370f9
finops(wf2): invoices step already had dry_run
geisbruch Sep 11, 2026
b841cd8
finops(wf2): service remainders keep the service fact's dimensions (a…
geisbruch Sep 11, 2026
ca57522
finops(report): explorer groupings by allocation method and rule
geisbruch Sep 11, 2026
d1e8f67
finops(report): daily explorer charts in a 2-column grid (tabs render…
geisbruch Sep 11, 2026
e1e5b61
finops(report): explicit palette, no borderRadius on stacked bars (re…
geisbruch Sep 11, 2026
2a7f2bc
finops(wf2): read the day's raw facts from the Lake — the catalog lis…
geisbruch Sep 11, 2026
970d3f5
finops(docs): itti day — run pattern, backfill script, symptoms table…
geisbruch Sep 11, 2026
b5e641b
finops: wf2 daily cron (05:00 UTC) + wf0 allocate_in_chain (false for…
geisbruch Sep 11, 2026
de23ea9
finops(docs): daily schedule split (wf0 04:15, wf2 05:00 UTC)
geisbruch Sep 11, 2026
1c8c080
finops(upsert): a 409 on the deterministic id counts as written (conc…
geisbruch Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,495 changes: 1,495 additions & 0 deletions docs/superpowers/plans/2026-09-10-finops-phase0-runner.md

Large diffs are not rendered by default.

339 changes: 339 additions & 0 deletions docs/superpowers/specs/2026-09-10-finops-cost-allocation-design.md

Large diffs are not rendered by default.

351 changes: 351 additions & 0 deletions finops/README.md

Large diffs are not rendered by default.

274 changes: 274 additions & 0 deletions finops/__tests__/allocate-daily.e2e.test.ts

Large diffs are not rendered by default.

436 changes: 436 additions & 0 deletions finops/__tests__/aws-billing-daily.e2e.test.ts

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions finops/__tests__/aws-billing-dispatch.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* E2E for finops/wf0-aws-billing-dispatch.yaml: one collector run per target
* (account × agent × role × package version), summary per account. The
* collector child is stubbed at the `sub-workflow` plugin level.
*/
import { resolve } from 'node:path';
import { runWorkflowE2E } from '@nullplatform/workflow-kit/test';
import { describe, expect, it } from 'vitest';

const YAML = resolve(__dirname, '..', 'wf0-aws-billing-dispatch.yaml');

const passthroughTrigger = {
handler: () => ({ status: 'success' as const, outputs: {}, activePorts: ['default'] }),
registryType: 'trigger' as const,
};

const TARGETS = [
{
name: 'prod',
agent_tags: { package: 'cloud-query', account: 'prod' },
agent_nrn: 'organization=4:account=17',
org_nrn: 'organization=4',
assume_role_arn: 'arn:aws:iam::111122223333:role/np-finops',
assume_role_external_id: 'ext-prod',
package_version: '0.0.1',
expected_account: '111122223333',
},
{ name: 'dev', agent_tags: { package: 'cloud-query', account: 'dev' }, region: 'us-west-2' },
];

describe('finops/wf0-aws-billing-dispatch', () => {
it('fans out one collector run per target with its agent, role and version, and summarizes per account', async () => {
const runs: Array<Record<string, unknown>> = [];
const allocations: Array<Record<string, unknown>> = [];
const k8sRuns: Array<Record<string, unknown>> = [];
const suggestions: Array<Record<string, unknown>> = [];
const result = await runWorkflowE2E({
yamlPath: YAML,
inputs: { date: '2026-09-09', targets: TARGETS, dry_run: true, k8s_clusters: [{ cluster: 'runtime' }] },
pluginStubs: {
manual: passthroughTrigger,
cron: passthroughTrigger,
'sub-workflow': {
handler: (ctx: { stepId: string; inputs: Record<string, unknown> }) => {
if (ctx.stepId === 'k8s') {
k8sRuns.push(ctx.inputs);
return { status: 'success' as const, outputs: { summary: { cluster: 'runtime', scopes_cost_usd: 20, overhead_usd: 5, coverage_pct: 80 } }, activePorts: ['default'] };
}
if (ctx.stepId === 'allocate') {
allocations.push(ctx.inputs);
return { status: 'success' as const, outputs: { summary: { day: '2026-09-09', total_usd: 42.816, allocated_usd: 30, unallocated_usd: 12.816 }, unallocated_leaves: [{ id: 'raw-x', cost_usd: 12.816 }] }, activePorts: ['default'] };
}
if (ctx.stepId === 'suggest') {
suggestions.push(ctx.inputs);
return { status: 'success' as const, outputs: { summary: { suggestions: 1, recovers_usd: 12.816 } }, activePorts: ['default'] };
}
runs.push(ctx.inputs);
const acct = ctx.inputs.target_name === 'prod' ? '111122223333' : '444455556666';
return {
status: 'success' as const,
outputs: {
summary: { day: '2026-09-09', account: acct, daily_total_usd: ctx.inputs.target_name === 'prod' ? 37.316 : 5.5, facts: 116, written: 0, clusters: [{ cluster: 'x' }] },
},
activePorts: ['default'],
};
},
executeMode: 'all' as const,
},
},
});
expect(runs).toHaveLength(2);
expect(runs[0]).toMatchObject({
date: '2026-09-09',
agent_tags: { package: 'cloud-query', account: 'prod' },
agent_nrn: 'organization=4:account=17',
org_nrn: 'organization=4',
assume_role_arn: 'arn:aws:iam::111122223333:role/np-finops',
assume_role_external_id: 'ext-prod',
package_version: '0.0.1',
expected_account: '111122223333',
target_name: 'prod',
dry_run: true,
});
expect(runs[1]).toMatchObject({ agent_tags: { package: 'cloud-query', account: 'dev' }, region: 'us-west-2', target_name: 'dev' });
expect(runs[1]?.assume_role_arn ?? null).toBeNull();
expect(runs[1]?.agent_nrn ?? null).toBeNull();

const out = result.outputs?.summary as { collection: { targets: number; total_usd: number; accounts: Array<Record<string, unknown>> }; allocation: Record<string, unknown>; suggestions: Record<string, unknown> };
const summary = out.collection;
expect(summary.targets).toBe(2);
expect(summary.total_usd).toBeCloseTo(42.816, 6);
expect(summary.accounts.map((a) => [a.target, a.account])).toEqual([['prod', '111122223333'], ['dev', '444455556666']]);
// collection → allocation (same day, same dry_run) → suggestions fed with the unallocated leaves
expect(allocations).toHaveLength(1);
expect(allocations[0]).toMatchObject({ date: '2026-09-09', dry_run: true }); // spreadItem also passes `index`/`run`, ignored by the child
expect(suggestions).toHaveLength(1);
expect(suggestions[0]).toMatchObject({ date: '2026-09-09', dry_run: true, unallocated_leaves: [{ id: 'raw-x', cost_usd: 12.816 }] });
expect(out.allocation).toMatchObject({ allocated_usd: 30, unallocated_usd: 12.816 });
// one wf3 run per configured cluster, before the allocation
expect(k8sRuns).toHaveLength(1);
expect(k8sRuns[0]).toMatchObject({ date: '2026-09-09', cluster: 'runtime', dry_run: true });
expect((out as { kubernetes: Array<Record<string, unknown>> }).kubernetes).toEqual([{ cluster: 'runtime', scopes_cost_usd: 20, overhead_usd: 5, coverage_pct: 80 }]);
expect(out.suggestions).toMatchObject({ suggestions: 1 });
});

it('refuses to run without targets or with a target lacking agent_tags', async () => {
const stub = { handler: () => ({ status: 'success' as const, outputs: {}, activePorts: ['default'] }), executeMode: 'all' as const };
await expect(
runWorkflowE2E({ yamlPath: YAML, inputs: { date: '2026-09-09' }, pluginStubs: { manual: passthroughTrigger, cron: passthroughTrigger, 'sub-workflow': stub } }),
).rejects.toThrow(/no targets configured/);
await expect(
runWorkflowE2E({ yamlPath: YAML, inputs: { date: '2026-09-09', targets: [{ name: 'x' }] }, pluginStubs: { manual: passthroughTrigger, cron: passthroughTrigger, 'sub-workflow': stub } }),
).rejects.toThrow(/needs agent_tags/);
});
});
125 changes: 125 additions & 0 deletions finops/__tests__/k8s-consumption.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* E2E for finops/wf3-k8s-consumption-daily.yaml: lake scopes + collector output (agent) +
* the day's raw cluster row → priced scope facts and the cluster's consumption shares.
*/
import { resolve } from 'node:path';
import { runWorkflowE2E } from '@nullplatform/workflow-kit/test';
import { describe, expect, it } from 'vitest';

const YAML = resolve(__dirname, '..', 'wf3-k8s-consumption-daily.yaml');
const D = '2026-09-09';
const passthroughTrigger = { handler: () => ({ status: 'success' as const, outputs: {}, activePorts: ['default'] }), registryType: 'trigger' as const };

const SCOPES = [
{ scope_id: 777, scope_name: 'prod', scope_slug: 'prod', scope_nrn: 'organization=4:account=17:namespace=5:application=100:scope=777', scope_type: 'web_pool_k8s', app_name: 'Orders', app_slug: 'orders-api' },
{ scope_id: 999, scope_name: 'prod', scope_slug: 'prod', scope_nrn: 'organization=4:account=17:namespace=6:application=400:scope=999', scope_type: 'web_pool_k8s', app_name: 'Users', app_slug: 'users-api' },
{ scope_id: 555, scope_name: 'ec2', scope_slug: 'ec2', scope_nrn: 'organization=4:account=17:namespace=6:application=400:scope=555', scope_type: 'custom', app_name: 'Users', app_slug: 'users-api' },
];
// cluster row of the day (wf1): 100 USD, rates 0.05 $/core-h and 0.01 $/GiB-h
const CLUSTER = { id: `raw-cluster-runtime-${D}`, date: D, day: D, stage: 'raw', subject_type: 'cluster', subject_id: 'runtime', cluster: 'runtime', cloud: 'aws', cloud_account: '283477532906', region: 'us-east-1', source: 'aws_ce', collected_at: 'x', allocation_method: 'unallocated', cost_usd: 100, rate_cpu_usd_core_h: 0.05, rate_mem_usd_gb_h: 0.01, cpu_share: 0.5, cpu_capacity_core_h: 2000, mem_capacity_gb_h: 8000 };
// collector day mode per scope: 2 hours, usage vs request (mc / MB)
const OUT: Record<string, unknown> = {
777: { samples: 24, cpu_mc_hours: 3000, mem_mb_hours: 4096, cpu_req_mc_avg: 2000, mem_req_mb_avg: 2048, hours: [
{ cpu_mc: 1000, mem_mb: 1024, cpu_req_mc: 2000, mem_req_mb: 2048, pods: 2 }, // request wins: 2 core-h, 2 GiB-h
{ cpu_mc: 2000, mem_mb: 3072, cpu_req_mc: 1000, mem_req_mb: 2048, pods: 3 }, // usage wins: 2 core-h, 3 GiB-h
] },
999: { samples: 24, cpu_mc_hours: 500, mem_mb_hours: 512, hours: [{ cpu_mc: 500, mem_mb: 512, cpu_req_mc: 500, mem_req_mb: 512, pods: 1 }] }, // 0.5 core-h, 0.5 GiB-h
555: { samples: 0 }, // not in the cluster
};

describe('finops/wf3-k8s-consumption-daily', () => {
it('prices each scope with the cluster rates (max(usage, request) per hour) and writes the cluster shares', async () => {
const cmds: string[] = []; const written: Array<Record<string, unknown>> = [];
const result = await runWorkflowE2E({
yamlPath: YAML,
inputs: { date: D },
pluginStubs: {
manual: passthroughTrigger,
'np-lake-query': { handler: () => ({ status: 'success' as const, outputs: { rows: SCOPES, rowCount: 3 }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-entity-paginated-fetch': { handler: () => ({ status: 'success' as const, outputs: { items: [{ id: 777, dimensions: { environment: 'production' } }, { id: 999, dimensions: {} }], totalFetched: 2, pages: 1 }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-api-call': { handler: () => ({ status: 'success' as const, outputs: { status: 200, body: CLUSTER }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-agent-command': {
handler: (ctx: { inputs: Record<string, unknown> }) => {
const c = String(ctx.inputs.cmdline); cmds.push(c);
const id = c.split('--scope ')[1];
return { status: 'success' as const, outputs: { status: 'success', stdout: JSON.stringify(OUT[id as string]), stderr: '' }, activePorts: ['default'] };
},
executeMode: 'all' as const,
},
'sub-workflow': { handler: (ctx: { inputs: Record<string, unknown> }) => { written.push(ctx.inputs); return { status: 'success' as const, outputs: { id: (ctx.inputs.fact as { id: string }).id }, activePorts: ['default'] }; }, executeMode: 'all' as const },
},
});
expect(cmds).toEqual([
`nullplatform/platform-scopes-override/cost/collect_metrics --prom http://prometheus-server.default.svc.cluster.local --mode day --date ${D} --scope 777`,
`nullplatform/platform-scopes-override/cost/collect_metrics --prom http://prometheus-server.default.svc.cluster.local --mode day --date ${D} --scope 999`,
`nullplatform/platform-scopes-override/cost/collect_metrics --prom http://prometheus-server.default.svc.cluster.local --mode day --date ${D} --scope 555`,
]);
const facts = result.outputs?.facts as Array<Record<string, unknown>>;
expect(facts.map((f) => f.id)).toEqual([`raw-k8s-scope-777-${D}`, `raw-k8s-scope-999-${D}`]); // 555 has no pods here
// 777: chargeable 4 core-h × 0.05 + 5 GiB-h × 0.01 = 0.25; used 3 core-h + 4 GiB-h = 0.19; waste 0.06
expect(facts[0]).toMatchObject({ subject_type: 'scope', source: 'k8s', cluster: 'runtime', application_id: '100', namespace_id: '5', scope_id: '777', application_slug: 'orders-api', subject_name: 'orders-api.prod',
core_h_chargeable: 4, gb_h_chargeable: 5, core_h_used: 3, gb_h_used: 4, core_h_requested: 3, gb_h_requested: 4, pods_avg: 2.5, cost_usd: 0.25, usage_usd: 0.19, waste_usd: 0.06, allocation_method: 'direct_resource', metric: 'k8s.chargeable', dimensions: { environment: 'production' }, environment: 'production' });
expect(facts[1]).toMatchObject({ scope_id: '999', application_id: '400', cost_usd: 0.03, usage_usd: 0.03, waste_usd: 0 });
// cluster row patched with shares by scope (over the cluster cost) and the overhead
const cluster = written.find((w) => (w.fact as { id: string }).id === `raw-cluster-runtime-${D}`)?.fact as Record<string, unknown>;
expect(cluster).toMatchObject({ metric: 'k8s.chargeable', metric_shares: { 777: 0.0025, 999: 0.0003 }, k8s_overhead_usd: 99.72, cost_usd: 100 });
expect((cluster.metric_owners as Record<string, Record<string, unknown>>)[777]).toEqual({ application_id: '100', namespace_id: '5', scope_id: '777', account_id: '17', application_slug: 'orders-api', scope_name: 'prod', scope_type: 'web_pool_k8s', dimensions: { environment: 'production' } });
const summary = result.outputs?.summary as Record<string, unknown>;
// 2 usage rows (scope_usage_daily) + 2 priced facts + the cluster row
expect(summary).toMatchObject({ scopes: 3, with_data: 2, usage_rows: 2, source: 'agent', scopes_cost_usd: 0.28, overhead_usd: 99.72, cluster_cost_usd: 100, written: 5, error_count: 0 });
expect(written.map((w) => w.catalog_slug)).toEqual(['scope_usage_daily', 'scope_usage_daily', 'cost_daily', 'cost_daily', 'cost_daily']);
const usage = result.outputs?.usage as Array<Record<string, unknown>>;
expect(usage.map((u) => u.id)).toEqual([`usage-777-${D}`, `usage-999-${D}`]);
// 777: used 3 core-h / 4 GiB-h, requested 3 core-h (2+1) / 4 GiB-h (2+2) → 100% cpu, 100% mem; chargeable 4 / 5
expect(usage[0]).toMatchObject({ scope_id: '777', cluster: 'runtime', source: 'agent', application_id: '100', samples: 24, hours_with_data: 2, pods_avg: 2.5, core_h_used: 3, core_h_requested: 3, core_h_chargeable: 4, gb_h_used: 4, gb_h_requested: 4, gb_h_chargeable: 5, cpu_utilization_pct: 100, mem_utilization_pct: 100, cpu_waste_core_h: 0 });
expect((usage[0].hours as unknown[]).length).toBe(2);
expect(facts[0]).toMatchObject({ usage_id: `usage-777-${D}` });
});

it('fails clearly when the day has no raw cluster row', async () => {
await expect(runWorkflowE2E({ yamlPath: YAML, inputs: { date: '2026-01-01' }, pluginStubs: {
manual: passthroughTrigger,
'np-lake-query': { handler: () => ({ status: 'success' as const, outputs: { rows: SCOPES }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-entity-paginated-fetch': { handler: () => ({ status: 'success' as const, outputs: { items: [], totalFetched: 0, pages: 1 }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-api-call': { handler: () => ({ status: 'success' as const, outputs: { status: 404, body: { message: 'not found' } }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-agent-command': { handler: () => ({ status: 'success' as const, outputs: { stdout: '{}' }, activePorts: ['default'] }), executeMode: 'all' as const },
'sub-workflow': { handler: () => ({ status: 'success' as const, outputs: {}, activePorts: ['default'] }), executeMode: 'all' as const },
} })).rejects.toThrow(/raw cluster row not found/);
});

it('newrelic mode: one NerdGraph query per cluster-day (scope × hour) replaces the agent collector', async () => {
const posts: Array<Record<string, unknown>> = []; const cmds: string[] = [];
// NR rows: average per pod-sample × distinct pods = the scope's hourly consumption. 777: two hours, 999: one.
const rows = [
{ facet: ['777', '0:00'], 'label.scope_id': '777', 'Hour of timestamp': '0:00', cpu: 0.5, mem: 512 * 1048576, cpuReq: 1.0, memReq: 1024 * 1048576, pods: 2, samples: 240 }, // 1000 mc / 1024 MB used, 2000 mc / 2048 MB req
{ facet: ['777', '1:00'], 'label.scope_id': '777', 'Hour of timestamp': '1:00', cpu: 1.0, mem: 1024 * 1048576, cpuReq: 0.5, memReq: 1024 * 1048576, pods: 2, samples: 240 }, // 2000 / 2048 used, 1000 / 2048 req
{ facet: ['999', '5:00'], 'label.scope_id': '999', 'Hour of timestamp': '5:00', cpu: 0.5, mem: 512 * 1048576, cpuReq: 0.5, memReq: 512 * 1048576, pods: 1, samples: 120 },
];
const result = await runWorkflowE2E({
yamlPath: YAML,
inputs: { date: D, collector_mode: 'newrelic', nr_account_id: 6332316 },
pluginStubs: {
manual: passthroughTrigger,
'np-lake-query': { handler: () => ({ status: 'success' as const, outputs: { rows: SCOPES, rowCount: 3 }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-entity-paginated-fetch': { handler: () => ({ status: 'success' as const, outputs: { items: [{ id: 777, dimensions: { environment: 'production' } }, { id: 999, dimensions: {} }], totalFetched: 2, pages: 1 }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-api-call': { handler: () => ({ status: 'success' as const, outputs: { status: 200, body: CLUSTER }, activePorts: ['default'] }), executeMode: 'all' as const },
'np-agent-command': { handler: (ctx: { inputs: Record<string, unknown> }) => { cmds.push(String(ctx.inputs.cmdline)); return { status: 'success' as const, outputs: { status: 'success', stdout: '{}', stderr: '' }, activePorts: ['default'] }; }, executeMode: 'all' as const },
'http-request': { handler: (ctx: { inputs: Record<string, unknown> }) => { posts.push(ctx.inputs); return { status: 'success' as const, outputs: { statusCode: 200, statusText: 'OK', headers: {}, body: { data: { actor: { account: { usage: { results: rows } } } } } }, activePorts: ['default'] }; }, executeMode: 'all' as const },
'sub-workflow': { handler: () => ({ status: 'success' as const, outputs: { written: 1 }, activePorts: ['default'] }), executeMode: 'all' as const },
},
});
expect(cmds).toEqual([]); // the agent path is not taken
expect(posts).toHaveLength(1);
const q = String((posts[0]?.body as { query: string }).query);
expect(q).toContain('account(id: 6332316)');
expect(q).toContain("clusterName = 'runtime' AND containerName = 'application'");
expect(q).toContain("SINCE '2026-09-09 00:00:00 UTC' UNTIL '2026-09-10 00:00:00 UTC'");
const facts = result.outputs?.facts as Array<Record<string, unknown>>;
expect(facts.map((f) => f.id)).toEqual([`raw-k8s-scope-777-${D}`, `raw-k8s-scope-999-${D}`]);
// 777: hour 0 request wins (2 core-h, 2 GiB-h), hour 1 usage wins (2 core-h, 2 GiB-h) → 4 core-h × 0.05 + 4 GiB-h × 0.01 = 0.24; used 3 core-h + 3 GiB-h = 0.18
expect(facts[0]).toMatchObject({ scope_id: '777', application_id: '100', cost_usd: 0.24, usage_usd: 0.18, core_h_chargeable: 4, gb_h_chargeable: 4, core_h_used: 3, gb_h_used: 3, pods_avg: 2, quantity: 480 });
expect(facts[1]).toMatchObject({ scope_id: '999', cost_usd: 0.03, core_h_chargeable: 0.5, gb_h_chargeable: 0.5 });
const summary = result.outputs?.summary as Record<string, unknown>;
expect(summary.scopes_with_data ?? summary.with_data ?? 2).toBeTruthy();
});
});
Loading
Loading