@malloy-publisher/server 0.0.224 → 0.0.226
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/app/api-doc.yaml +76 -13
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +83 -16
- package/dist/server.mjs +249 -46
- package/dist/sshcrypto-8m50vnmb.node +0 -0
- package/package.json +1 -1
- package/src/controller/materialization.controller.spec.ts +72 -0
- package/src/controller/materialization.controller.ts +75 -11
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/package_load/package_load_pool.ts +5 -1
- package/src/package_load/package_load_worker.ts +7 -0
- package/src/package_load/protocol.ts +5 -1
- package/src/service/build_plan.spec.ts +110 -9
- package/src/service/build_plan.ts +126 -7
- package/src/service/environment.ts +4 -0
- package/src/service/materialization_service.spec.ts +42 -0
- package/src/service/materialization_service.ts +40 -2
- package/src/service/materialization_test_fixtures.ts +46 -15
- package/src/service/package.ts +116 -32
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/storage/DatabaseInterface.ts +1 -0
- package/tests/fixtures/persist-multi-level/data/orders.csv +5 -0
- package/tests/fixtures/persist-multi-level/multi_level.malloy +18 -0
- package/tests/fixtures/persist-multi-level/publisher.json +5 -0
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +251 -0
- package/src/service/materialization_cron_gate.spec.ts +0 -142
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { RestE2EEnv, startRestE2E } from "../../harness/rest_e2e";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PROJECT_NAME = "test-project-refmanifest";
|
|
12
|
+
const PACKAGE_NAME = "persist-multi-level";
|
|
13
|
+
const API = `/api/v0/environments/${PROJECT_NAME}/packages/${PACKAGE_NAME}`;
|
|
14
|
+
|
|
15
|
+
const TERMINAL_STATUSES = ["MANIFEST_FILE_READY", "FAILED", "CANCELLED"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WI-1 (Phase D): the orchestrated build seeds its build Manifest from
|
|
19
|
+
* `referenceManifest` and honors `strictUpstreams`. Proven end-to-end over a
|
|
20
|
+
* two-level persist DAG (`orders_base` -> `orders_rollup`):
|
|
21
|
+
*
|
|
22
|
+
* - Build the downstream `orders_rollup` ALONE with a `referenceManifest`
|
|
23
|
+
* pointing at the already-materialized `orders_base` table and
|
|
24
|
+
* `strictUpstreams=true` -> the build SUCCEEDS, which under strict mode is
|
|
25
|
+
* only possible if the upstream reference resolved to the physical table
|
|
26
|
+
* (a strict miss would throw).
|
|
27
|
+
* - Build `orders_rollup` ALONE under `strictUpstreams=true` with NO
|
|
28
|
+
* `referenceManifest` -> the build FAILS loudly (runtime-manifest-strict-miss)
|
|
29
|
+
* instead of silently recomputing the upstream live.
|
|
30
|
+
*/
|
|
31
|
+
describe("Materialization reference manifest + strictUpstreams (E2E)", () => {
|
|
32
|
+
let env: (RestE2EEnv & { stop(): Promise<void> }) | null = null;
|
|
33
|
+
let baseUrl: string;
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
env = await startRestE2E();
|
|
37
|
+
baseUrl = env.baseUrl;
|
|
38
|
+
|
|
39
|
+
const fixtureDir = path.resolve(
|
|
40
|
+
__dirname,
|
|
41
|
+
"../../fixtures/persist-multi-level",
|
|
42
|
+
);
|
|
43
|
+
const createRes = await fetch(`${baseUrl}/api/v0/environments`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
name: PROJECT_NAME,
|
|
48
|
+
packages: [{ name: PACKAGE_NAME, location: fixtureDir }],
|
|
49
|
+
connections: [],
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
if (!createRes.ok) {
|
|
53
|
+
const body = await createRes.text();
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to create test project (${createRes.status}): ${body}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const deadline = Date.now() + 30_000;
|
|
60
|
+
let pkgReady = false;
|
|
61
|
+
while (!pkgReady && Date.now() < deadline) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${baseUrl}${API}`);
|
|
64
|
+
if (res.ok) {
|
|
65
|
+
pkgReady = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// not ready yet
|
|
70
|
+
}
|
|
71
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
72
|
+
}
|
|
73
|
+
if (!pkgReady) {
|
|
74
|
+
throw new Error("Test package did not become available in time");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
if (baseUrl) {
|
|
80
|
+
try {
|
|
81
|
+
await fetch(`${baseUrl}/api/v0/environments/${PROJECT_NAME}`, {
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
// best-effort cleanup
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await env?.stop();
|
|
89
|
+
env = null;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function url(p: string): string {
|
|
93
|
+
return `${baseUrl}${API}${p}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function createMaterialization(
|
|
97
|
+
body: Record<string, unknown> = {},
|
|
98
|
+
): Promise<Response> {
|
|
99
|
+
return fetch(url("/materializations"), {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify(body),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function pollUntilTerminal(
|
|
107
|
+
id: string,
|
|
108
|
+
timeoutMs = 90_000,
|
|
109
|
+
): Promise<Record<string, unknown>> {
|
|
110
|
+
const deadline = Date.now() + timeoutMs;
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
const res = await fetch(url(`/materializations/${id}`));
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
115
|
+
if (TERMINAL_STATUSES.includes(data.status as string)) return data;
|
|
116
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Materialization ${id} did not reach a terminal state`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Delete a terminal materialization record (optionally dropping its tables). */
|
|
122
|
+
async function deleteRun(id: string, dropTables = false): Promise<void> {
|
|
123
|
+
await fetch(
|
|
124
|
+
url(`/materializations/${id}${dropTables ? "?dropTables=true" : ""}`),
|
|
125
|
+
{ method: "DELETE" },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Build one source alone and drive it to a terminal state. */
|
|
130
|
+
async function buildOneAlone(
|
|
131
|
+
body: Record<string, unknown>,
|
|
132
|
+
): Promise<Record<string, unknown>> {
|
|
133
|
+
const createRes = await createMaterialization(body);
|
|
134
|
+
expect(createRes.status).toBe(201);
|
|
135
|
+
const { id } = (await createRes.json()) as { id: string };
|
|
136
|
+
const settled = await pollUntilTerminal(id);
|
|
137
|
+
return { id, ...settled };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The planned sources keyed by name (off Package.buildPlan). */
|
|
141
|
+
async function planSourcesByName(): Promise<
|
|
142
|
+
Record<string, Record<string, unknown>>
|
|
143
|
+
> {
|
|
144
|
+
const res = await fetch(url(""));
|
|
145
|
+
expect(res.status).toBe(200);
|
|
146
|
+
const pkg = (await res.json()) as Record<string, unknown>;
|
|
147
|
+
const plan = pkg.buildPlan as Record<string, unknown>;
|
|
148
|
+
expect(plan).toBeDefined();
|
|
149
|
+
const sources = plan.sources as Record<string, Record<string, unknown>>;
|
|
150
|
+
const byName: Record<string, Record<string, unknown>> = {};
|
|
151
|
+
for (const s of Object.values(sources)) {
|
|
152
|
+
byName[s.name as string] = s;
|
|
153
|
+
}
|
|
154
|
+
return byName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const UPSTREAM_TABLE = "orders_base_built";
|
|
158
|
+
|
|
159
|
+
it(
|
|
160
|
+
"builds a downstream source alone, referencing the upstream's physical table under strict",
|
|
161
|
+
async () => {
|
|
162
|
+
const sources = await planSourcesByName();
|
|
163
|
+
const base = sources["orders_base"];
|
|
164
|
+
const rollup = sources["orders_rollup"];
|
|
165
|
+
expect(base).toBeDefined();
|
|
166
|
+
expect(rollup).toBeDefined();
|
|
167
|
+
|
|
168
|
+
// 1) Materialize the upstream `orders_base` into a known physical table.
|
|
169
|
+
const baseRun = await buildOneAlone({
|
|
170
|
+
buildInstructions: {
|
|
171
|
+
sources: [
|
|
172
|
+
{
|
|
173
|
+
sourceEntityId: base.sourceEntityId,
|
|
174
|
+
sourceID: base.sourceID,
|
|
175
|
+
materializedTableId: "mt-base",
|
|
176
|
+
physicalTableName: UPSTREAM_TABLE,
|
|
177
|
+
realization: "COPY",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
expect(baseRun.status).toBe("MANIFEST_FILE_READY");
|
|
183
|
+
await deleteRun(baseRun.id as string); // keep the physical table
|
|
184
|
+
|
|
185
|
+
// 2) Build the downstream `orders_rollup` ALONE, referencing the
|
|
186
|
+
// upstream's physical table under strictUpstreams. Success is only
|
|
187
|
+
// possible if the reference resolved (strict forbids the live fallback).
|
|
188
|
+
const rollupRun = await buildOneAlone({
|
|
189
|
+
buildInstructions: {
|
|
190
|
+
sources: [
|
|
191
|
+
{
|
|
192
|
+
sourceEntityId: rollup.sourceEntityId,
|
|
193
|
+
sourceID: rollup.sourceID,
|
|
194
|
+
materializedTableId: "mt-rollup",
|
|
195
|
+
physicalTableName: "orders_rollup_built",
|
|
196
|
+
realization: "COPY",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
referenceManifest: [
|
|
200
|
+
{
|
|
201
|
+
sourceEntityId: base.sourceEntityId,
|
|
202
|
+
physicalTableName: UPSTREAM_TABLE,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
strictUpstreams: true,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
expect(rollupRun.status).toBe("MANIFEST_FILE_READY");
|
|
209
|
+
|
|
210
|
+
// Cleanup both physical tables and records.
|
|
211
|
+
await deleteRun(rollupRun.id as string, true);
|
|
212
|
+
// Drop the upstream table via a fresh no-op build+drop is overkill;
|
|
213
|
+
// the environment teardown removes the project. Best-effort direct drop
|
|
214
|
+
// is unnecessary for correctness here.
|
|
215
|
+
},
|
|
216
|
+
{ timeout: 120_000 },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
it(
|
|
220
|
+
"fails loudly when a strict upstream is neither built nor referenced",
|
|
221
|
+
async () => {
|
|
222
|
+
const sources = await planSourcesByName();
|
|
223
|
+
const rollup = sources["orders_rollup"];
|
|
224
|
+
expect(rollup).toBeDefined();
|
|
225
|
+
|
|
226
|
+
// Build `orders_rollup` ALONE under strictUpstreams with NO
|
|
227
|
+
// referenceManifest for its upstream -> the compile throws
|
|
228
|
+
// runtime-manifest-strict-miss and the run FAILS.
|
|
229
|
+
const run = await buildOneAlone({
|
|
230
|
+
buildInstructions: {
|
|
231
|
+
sources: [
|
|
232
|
+
{
|
|
233
|
+
sourceEntityId: rollup.sourceEntityId,
|
|
234
|
+
sourceID: rollup.sourceID,
|
|
235
|
+
materializedTableId: "mt-rollup-strict",
|
|
236
|
+
physicalTableName: "orders_rollup_strict",
|
|
237
|
+
realization: "COPY",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
strictUpstreams: true,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(run.status).toBe("FAILED");
|
|
245
|
+
expect(String(run.error ?? "")).toContain("manifest");
|
|
246
|
+
|
|
247
|
+
await deleteRun(run.id as string, true);
|
|
248
|
+
},
|
|
249
|
+
{ timeout: 120_000 },
|
|
250
|
+
);
|
|
251
|
+
});
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
|
|
6
|
-
import { Environment } from "./environment";
|
|
7
|
-
import type { Package } from "./package";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The publish gate for the package-level materialization cron. At Phase B the
|
|
11
|
-
* package-level cron (`materialization.schedule`) is replaced outright by
|
|
12
|
-
* `materialization.freshness.window` (persistence.md §9.4), so ANY declared cron
|
|
13
|
-
* is rejected regardless of the sources' sharing. The `sharing=private`
|
|
14
|
-
* carve-out arrives whole with Phase A (persistence-phase-b-design.md §3.4);
|
|
15
|
-
* until then there is no private artifact for a cron to own, so the B→A rule is
|
|
16
|
-
* reject-all. These tests run a real `Environment` + `Package.create` over temp
|
|
17
|
-
* dirs, so they also prove end-to-end that the pinned compiler ACCEPTS
|
|
18
|
-
* `sharing=` / `refresh=` keys in the `#@ persist` annotation and that the
|
|
19
|
-
* values survive to the wire build plan verbatim (unset stays null — never
|
|
20
|
-
* defaulted to "shared").
|
|
21
|
-
*/
|
|
22
|
-
describe("materialization cron gate", () => {
|
|
23
|
-
let rootDir: string;
|
|
24
|
-
let envPath: string;
|
|
25
|
-
|
|
26
|
-
async function loadPackage(model: string, schedule?: string) {
|
|
27
|
-
const dir = path.join(envPath, "pkg");
|
|
28
|
-
await fs.mkdir(dir, { recursive: true });
|
|
29
|
-
await fs.writeFile(
|
|
30
|
-
path.join(dir, "publisher.json"),
|
|
31
|
-
JSON.stringify({
|
|
32
|
-
name: "pkg",
|
|
33
|
-
description: "fixture",
|
|
34
|
-
...(schedule ? { materialization: { schedule } } : {}),
|
|
35
|
-
}),
|
|
36
|
-
);
|
|
37
|
-
await fs.writeFile(path.join(dir, "model.malloy"), model);
|
|
38
|
-
const env = await Environment.create("testEnv", envPath, []);
|
|
39
|
-
await env.addPackage("pkg");
|
|
40
|
-
return env.getPackage("pkg", false);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function sourceByName(pkg: Package, name: string) {
|
|
44
|
-
const sources = pkg.getBuildPlan()?.sources ?? {};
|
|
45
|
-
const found = Object.values(sources).find((s) => s.name === name);
|
|
46
|
-
if (!found) throw new Error(`persist source '${name}' not in build plan`);
|
|
47
|
-
return found;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeEach(async () => {
|
|
51
|
-
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "publisher-cron-"));
|
|
52
|
-
envPath = path.join(rootDir, "env");
|
|
53
|
-
await fs.mkdir(envPath, { recursive: true });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
afterEach(async () => {
|
|
57
|
-
await fs.rm(rootDir, { recursive: true, force: true }).catch(() => {});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const MIXED_MODEL = `##! experimental.persistence
|
|
61
|
-
|
|
62
|
-
#@ persist name="priv_table" sharing=private refresh=incremental
|
|
63
|
-
source: priv is duckdb.sql("SELECT 1 as x")
|
|
64
|
-
|
|
65
|
-
#@ persist name="open_table" sharing=shared
|
|
66
|
-
source: open is duckdb.sql("SELECT 2 as x")
|
|
67
|
-
|
|
68
|
-
#@ persist name="unset_table"
|
|
69
|
-
source: unspecified is duckdb.sql("SELECT 3 as x")
|
|
70
|
-
`;
|
|
71
|
-
|
|
72
|
-
const ALL_PRIVATE_MODEL = `##! experimental.persistence
|
|
73
|
-
|
|
74
|
-
#@ persist name="a_table" sharing=private
|
|
75
|
-
source: a is duckdb.sql("SELECT 1 as x")
|
|
76
|
-
|
|
77
|
-
#@ persist name="b_table" sharing=private refresh=full
|
|
78
|
-
source: b is duckdb.sql("SELECT 2 as x")
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
it(
|
|
82
|
-
"surfaces declared sharing/refresh verbatim on the build plan (null when unset)",
|
|
83
|
-
async () => {
|
|
84
|
-
const pkg = await loadPackage(MIXED_MODEL);
|
|
85
|
-
|
|
86
|
-
const priv = sourceByName(pkg, "priv");
|
|
87
|
-
expect(priv.sharing).toBe("private");
|
|
88
|
-
expect(priv.refresh).toBe("incremental");
|
|
89
|
-
|
|
90
|
-
const open = sourceByName(pkg, "open");
|
|
91
|
-
expect(open.sharing).toBe("shared");
|
|
92
|
-
expect(open.refresh).toBeNull();
|
|
93
|
-
|
|
94
|
-
// Unset must be reported as null — distinguishable from an explicit
|
|
95
|
-
// "shared" — because the control plane applies the platform default
|
|
96
|
-
// (unset => shared) itself.
|
|
97
|
-
const unspecified = sourceByName(pkg, "unspecified");
|
|
98
|
-
expect(unspecified.sharing).toBeNull();
|
|
99
|
-
expect(unspecified.refresh).toBeNull();
|
|
100
|
-
},
|
|
101
|
-
{ timeout: 30000 },
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
it(
|
|
105
|
-
"rejects any declared cron, pointing at freshness.window",
|
|
106
|
-
async () => {
|
|
107
|
-
const pkg = await loadPackage(MIXED_MODEL, "0 6 * * *");
|
|
108
|
-
|
|
109
|
-
// One actionable message for the whole package — the B→A rule is
|
|
110
|
-
// reject-all, not per-source.
|
|
111
|
-
const warnings = pkg.scheduleWarnings();
|
|
112
|
-
expect(warnings).toHaveLength(1);
|
|
113
|
-
const joined = pkg.formatInvalidSchedule();
|
|
114
|
-
expect(joined).toContain("materialization.schedule");
|
|
115
|
-
expect(joined).toContain("materialization.freshness.window");
|
|
116
|
-
},
|
|
117
|
-
{ timeout: 30000 },
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
it(
|
|
121
|
-
"rejects a declared cron even when every persist source is private",
|
|
122
|
-
async () => {
|
|
123
|
-
// The sharing=private carve-out arrives with Phase A; during B→A even
|
|
124
|
-
// an all-private package's cron is rejected outright.
|
|
125
|
-
const pkg = await loadPackage(ALL_PRIVATE_MODEL, "0 6 * * *");
|
|
126
|
-
expect(pkg.scheduleWarnings()).toHaveLength(1);
|
|
127
|
-
expect(pkg.formatInvalidSchedule()).toContain(
|
|
128
|
-
"materialization.freshness.window",
|
|
129
|
-
);
|
|
130
|
-
},
|
|
131
|
-
{ timeout: 30000 },
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
it(
|
|
135
|
-
"is inert when no cron is declared, whatever the sources' sharing",
|
|
136
|
-
async () => {
|
|
137
|
-
const pkg = await loadPackage(MIXED_MODEL);
|
|
138
|
-
expect(pkg.scheduleWarnings()).toEqual([]);
|
|
139
|
-
},
|
|
140
|
-
{ timeout: 30000 },
|
|
141
|
-
);
|
|
142
|
-
});
|