diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index e229cdd..89262a4 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -93,6 +93,9 @@ func RunScenario(config Config, strategy Strategy) (Result, error) { return Result{}, fmt.Errorf("scenario: unknown strategy %q", strategy) } + if educated == nil { + educated = []int{} // a nil slice marshals to JSON null, not [] + } cascade := RunCascade(graph, config.Origin, config.ForwardProb, educated, thresholds) return Result{ Strategy: strategy, diff --git a/web/index.html b/web/index.html index 9e5fc8f..32253a8 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ - Vite App + spreadlab
diff --git a/web/src/App.vue b/web/src/App.vue index d05208d..342b1f1 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,47 +1,40 @@ diff --git a/web/src/assets/logo.svg b/web/src/assets/logo.svg deleted file mode 100644 index 7565660..0000000 --- a/web/src/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/src/components/ComparisonTable.vue b/web/src/components/ComparisonTable.vue new file mode 100644 index 0000000..eb88996 --- /dev/null +++ b/web/src/components/ComparisonTable.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/web/src/components/HelloWorld.vue b/web/src/components/HelloWorld.vue deleted file mode 100644 index a2eabd1..0000000 --- a/web/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/web/src/components/TheWelcome.vue b/web/src/components/TheWelcome.vue deleted file mode 100644 index 8b731d9..0000000 --- a/web/src/components/TheWelcome.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/web/src/components/WelcomeItem.vue b/web/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/web/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/web/src/components/__tests__/ComparisonTable.spec.ts b/web/src/components/__tests__/ComparisonTable.spec.ts new file mode 100644 index 0000000..810c3b4 --- /dev/null +++ b/web/src/components/__tests__/ComparisonTable.spec.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ComparisonTable from '../ComparisonTable.vue' +import type { ComparisonResponse } from '@/types/api' + +// A small hand-made comparison; the real numbers come from the API and are +// pinned by the Go tests. Here we only care that the table renders them. +const comparison: ComparisonResponse = { + config: { + numStudents: 10, + edgesPerNode: 2, + triangleProb: 0.4, + forwardProb: 0.5, + numEducated: 3, + origin: 0, + graphSeed: 1, + thresholdSeed: 2, + educationSeed: 3, + }, + results: [ + { + strategy: 'none', + educated: [], + reachedAtRound: [0, 1, 1, 2, 2, 2, 3, -1, -1, -1], + numReached: 7, + numRounds: 4, + reachedPct: 70, + }, + { + strategy: 'random', + educated: [2, 5, 8], + reachedAtRound: [0, 1, 1, 2, -1, -1, -1, -1, -1, -1], + numReached: 4, + numRounds: 3, + reachedPct: 40, + }, + { + strategy: 'most-connected', + educated: [1, 2, 3], + reachedAtRound: [0, 1, -1, -1, -1, -1, -1, -1, -1, -1], + numReached: 2, + numRounds: 2, + reachedPct: 20, + }, + ], +} + +describe('ComparisonTable', () => { + it('renders one row per strategy with its reach', () => { + const wrapper = mount(ComparisonTable, { props: { comparison } }) + + const rows = wrapper.findAll('tbody tr') + expect(rows).toHaveLength(3) + expect(rows[0]!.text()).toContain('No program') + expect(rows[0]!.text()).toContain('7 / 10') + expect(rows[2]!.text()).toContain('Educate the most connected') + expect(rows[2]!.text()).toContain('20%') + }) + + it('falls back to the raw strategy name for unknown strategies', () => { + const unknown: ComparisonResponse = { + config: comparison.config, + results: [{ ...comparison.results[0]!, strategy: 'telepathy' }], + } + const wrapper = mount(ComparisonTable, { props: { comparison: unknown } }) + expect(wrapper.find('tbody th').text()).toBe('telepathy') + }) +}) diff --git a/web/src/components/__tests__/HelloWorld.spec.ts b/web/src/components/__tests__/HelloWorld.spec.ts deleted file mode 100644 index 2533202..0000000 --- a/web/src/components/__tests__/HelloWorld.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, it, expect } from 'vitest' - -import { mount } from '@vue/test-utils' -import HelloWorld from '../HelloWorld.vue' - -describe('HelloWorld', () => { - it('renders properly', () => { - const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) - expect(wrapper.text()).toContain('Hello Vitest') - }) -}) diff --git a/web/src/components/icons/IconCommunity.vue b/web/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/web/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/web/src/components/icons/IconDocumentation.vue b/web/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/web/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/web/src/components/icons/IconEcosystem.vue b/web/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/web/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/web/src/components/icons/IconSupport.vue b/web/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/web/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/web/src/components/icons/IconTooling.vue b/web/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/web/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..7dd3210 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,27 @@ +import type { Config } from '@/types/engine' +import type { ComparisonResponse } from '@/types/api' + +// Thin typed wrappers around the JSON API. The types come from +// src/types/, which is generated from the Go structs (single source of +// truth); nothing here redefines a shape. + +async function requestJSON(input: string, init?: RequestInit): Promise { + const response = await fetch(input, init) + if (!response.ok) { + const body = await response.text() + throw new Error(`${response.status} ${response.statusText}: ${body}`) + } + return response.json() as Promise +} + +export function fetchDefaultConfig(): Promise { + return requestJSON('/api/config/default') +} + +export function runComparison(config: Config): Promise { + return requestJSON('/api/comparison', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config), + }) +} diff --git a/web/vite.config.ts b/web/vite.config.ts index 4217010..4939dcf 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -15,4 +15,11 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, + server: { + // The Go server owns /api; run it next to `npm run dev` with: + // go run ./cmd/spreadlab + proxy: { + '/api': 'http://localhost:8080', + }, + }, })